Skip to content

fix(timezone): propagate configured timezone to agent prompt and terminals#10061

Open
0xnoahzhu wants to merge 1 commit into
NousResearch:mainfrom
0xnoahzhu:fix/timestamp-includes-timezone
Open

fix(timezone): propagate configured timezone to agent prompt and terminals#10061
0xnoahzhu wants to merge 1 commit into
NousResearch:mainfrom
0xnoahzhu:fix/timestamp-includes-timezone

Conversation

@0xnoahzhu

Copy link
Copy Markdown

What does this PR do?

When a user configured a timezone (HERMES_TIMEZONE env var or timezone: key
in ~/.hermes/config.yaml), the agent didn't know about it. Two visible symptoms:

  1. The system prompt's timestamp had no zone marker, so the model fell back to
    server-local time. "What's today's date?" returned the server's date, not
    the user's.
  2. date (and any TZ-honouring runtime) inside the terminal tool reported the
    server's wall clock, so shell output disagreed with what the agent thought
    the time was.

Fixed end-to-end via two centralized injection points instead of touching each
backend individually:

  • Agent prompt (run_agent.py): append a Timezone: <IANA> (UTC±HH:MM)
    line to the timestamp block when a timezone is configured.
  • Every terminal backend: BaseEnvironment._wrap_command injects
    export TZ=<name> once. That covers local, docker, ssh, singularity, modal,
    and daytona — all six flow through this method. Managed Modal uses a different
    exec path (BaseModalExecutionEnvironment._prepare_modal_exec), so it gets
    the same injection there.
  • Resolver (tools/environments/base.py): new _resolve_configured_tz_name()
    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. This resolver covers them.

Related Issue

N/A — no existing issue. Reproduced locally with timezone: Asia/Shanghai in
~/.hermes/config.yaml running on a UTC server.

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • run_agent.py — emit Timezone: <IANA> (UTC±HH:MM) after the timestamp when
    configured. Uses hermes_time.now() so the offset is computed in the user's
    zone.
  • tools/environments/base.py — new _resolve_configured_tz_name() helper;
    BaseEnvironment._wrap_command injects export TZ=<name> after the snapshot
    source so it overrides anything inherited.
  • tools/environments/modal_utils.py
    BaseModalExecutionEnvironment._prepare_modal_exec prepends
    export TZ=<name>; after sudo wrapping (covers ManagedModalEnvironment).
  • tools/environments/local.py_make_run_env and _sanitize_subprocess_env
    set TZ=<name> 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. Refactored to use the shared
    helper.
  • Tests:
    • tests/run_agent/test_run_agent.pytest_includes_timezone_when_configured,
      test_omits_timezone_line_without_config.
    • tests/tools/test_base_environment.pyTestWrapCommandTimezone (4) and
      TestResolveConfiguredTzName (3).
    • tests/tools/test_modal_utils_tz.pyTestPrepareModalExecTimezone (4)
      [new file].
    • tests/tools/test_local_env_blocklist.pyTestTimezonePropagation (5).

How to Test

  1. Set timezone (pick one):
    • export HERMES_TIMEZONE=Asia/Shanghai, or
    • Add timezone: Asia/Shanghai to ~/.hermes/config.yaml (with no
      HERMES_TIMEZONE env var).
  2. Start hermes chat. The system prompt should include
    Timezone: Asia/Shanghai (UTC+08:00) right under the timestamp.
  3. In the chat, ask the agent to run date. Output should be Asia/Shanghai
    wall-clock time (CST), not the server's zone.
  4. Targeted unit tests:

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix: prefix; one squashed commit)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (one squashed commit, eight files)
  • I've run pytest on the affected modules and all tests pass (301 passed); full pytest tests/ not re-run end-to-end
  • I've added tests for my changes
  • I've tested on my platform: macOS 15 (Darwin 25.4.0)

Documentation & Housekeeping

  • I've updated relevant documentation — not done. Timezone configuration
    is currently undocumented anywhere on https://hermes-agent.nousresearch.com/docs
    (verified via the docs search). Worth a follow-up doc PR; this PR keeps
    its surface area to the bug.
  • I've updated cli-config.yaml.example if I added/changed config keys —
    N/A: no new config keys; the existing timezone key just starts
    working in places where it was silently ignored.
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture
    or workflows — N/A: no architecture changes.
  • I've considered cross-platform impact — export TZ=<name> is POSIX; the
    change works on Linux, macOS, Termux, and Git Bash on Windows since every
    terminal backend already shells through bash.
  • I've updated tool descriptions/schemas if I changed tool behavior —
    N/A: no tool schema or description changes.

@0xnoahzhu 0xnoahzhu changed the title fix: surface configured timezone in system prompt and all terminal ba… fix(): propagate configured timezone to agent prompt and terminals Apr 15, 2026
@0xnoahzhu 0xnoahzhu changed the title fix(): propagate configured timezone to agent prompt and terminals fix(timezone): propagate configured timezone to agent prompt and terminals Apr 15, 2026
@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 area/config Config system, migrations, profiles labels Apr 26, 2026
@markojak

Copy link
Copy Markdown

Linking this into the #17459/#17476 direction.

This remains useful adjacent work if it provides consistent user timezone resolution for the agent and terminal/runtime environments. Please keep it aligned with the chosen live-time context path:

Related: #10421, #15872, #17459, #17476.

@0xnoahzhu 0xnoahzhu force-pushed the fix/timestamp-includes-timezone branch from c61e6e1 to 14c67a4 Compare May 1, 2026 13:16
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>
@0xnoahzhu 0xnoahzhu force-pushed the fix/timestamp-includes-timezone branch from 14c67a4 to a324446 Compare May 1, 2026 13:24
@0xnoahzhu

Copy link
Copy Markdown
Author

Thanks for the review and the consolidation context in #17459/#17476. I've scoped this PR down to terminal TZ propagation only:

  • Reverted the Timezone: line in _build_system_prompt() — the cached system prompt stays byte-stable. The corresponding two prompt-timezone tests are gone too. Live time/timezone context for the agent should come from the ephemeral per-turn user-message path in fix: prevent stale timestamp perception by injecting current time per-turn #15872, not the cached prefix.
  • Kept the terminal TZ propagation (BaseEnvironment._wrap_command, the local.py env helpers, and the managed-Modal exec path). This is independently useful — it makes shell wall-clock match the user's configured zone — and is tested separately from prompt-cache behavior, no overlap with fix: prevent stale timestamp perception by injecting current time per-turn #15872.
  • Closed an adjacent gap in code_execution_tool.py: the local sandbox path read HERMES_TIMEZONE directly via os.getenv, silently missing config.yaml-only setups (the same bug class this PR fixes for terminal backends). Switched to hermes_time.get_timezone() so both paths resolve consistently. Also dropped a redundant inline TZ= prefix on the remote-backend code path — _wrap_command already exports TZ before the script runs.

Rebased onto latest main and squashed to a single commit. No changes to the cached system prompt or tool schemas, so nothing here interacts with the get_tool_definitions() cache work in #17335.

quinnmacro added a commit to quinnmacro/hermes-upstream-fork that referenced this pull request May 30, 2026
…-turn

The system prompt includes a timestamp frozen at session creation time
(for prompt cache stability), but labeled "Conversation started:" —
agents interpret this as the current time, leading to incorrect
time-sensitive behavior (e.g. saying "good night" at 9 AM in a session
started at 5 AM).

Split the timestamp into two layers, both using user-message injection
to preserve prompt cache:

| Layer | Content | Location | Frequency |
| System prompt | Session started: (timezone) | Frozen | Once per session |
| User message | Current time: (timezone) | Dynamic | Every turn |

Key changes:
1. agent/system_prompt.py: Rename 'Conversation started' → 'Session started',
   add IANA timezone name, drop minute precision for daily cache stability
2. agent/conversation_loop.py: Inject current time + timezone into
   _plugin_user_context on every turn (cache-safe)
3. agent/chat_completion_helpers.py: Inject current time into
   handle_max_iterations summary request user message
4. hermes_time.py: Add get_timezone_name() public API
5. Tests: 5 new/updated tests covering cache safety, user-message
   injection, and max-iterations path

Rebased onto upstream/main (v2026.5.29). Code adapted to 3-module
refactor (run_agent.py → agent/system_prompt.py + conversation_loop.py
+ chat_completion_helpers.py).

Approves maintainer alignment criteria:
- Volatile 'Current time' kept out of cached system prompt
- User timezone in ephemeral context (IANA name)
- Timezone resolution aligned with NousResearch#10061
- Tests prove cache stability while per-turn time updates
- No gateway-only timestamp-prefix mechanism
quinnmacro added a commit to quinnmacro/hermes-upstream-fork that referenced this pull request Jun 11, 2026
…-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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent loop, run_agent.py, prompt builder 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.

3 participants