hermes - ✅(Solved) Fix Rework quiet-hours/time awareness: surface time to agent/tools, don't enforce in control plane [10 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#17459Fetched 2026-04-30 06:47:31
View on GitHub
Comments
1
Participants
1
Timeline
23
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×18labeled ×4commented ×1

Error Message

  • If waiting would cross the user's normal contact window, warn the agent.
  • Warn if sending outside the window; do not block. The agent can explicitly say why it is sending now or choose to schedule/defer.

Root Cause

  • Closed, not merged / should not be revived as-is
    • #4711 feat: Add profile-scoped gateway autonomy runtime introduced gateway autonomy with quiet-hours controls in the control plane. It is closed and unmerged; do not resurrect the quiet-hours enforcement shape.
    • #7402 feat: drain Ockham outbox notifications via Hermes scheduler was closed and unmerged; it explicitly held approvals/digests during quiet hours. That is the wrong semantic for Hermes core unless the agent is told and chooses deferral.
  • Open time-awareness work to salvage / consolidate
    • #10421 captures the core need: turn-level live current time awareness.
    • #15872 is the preferred implementation shape: inject Current time + timezone into the user-message/ephemeral context path, preserving prompt-cache stability.
    • #10448 and #5241 address similar bugs but put live time into the system-prompt path; that should be avoided because it undermines prompt caching.
    • #10061 is adjacent and useful: propagate configured timezone to agent prompt/terminal environments.
    • #9784/#5487 are gateway-only timestamp-prefix variants; we should avoid multiple parallel timestamp mechanisms and consolidate on one core ephemeral context path.
  • Open caching regressions related to this work
    • #17335: get_tool_definitions() quiet-mode cache pollution causes duplicate LCM schemas in Gateway. This should be treated as an immediate bug fix.
    • #15866/#8687/#8689/#5713: system-prompt timestamp / prompt-cache invalidation concerns. The durable direction is stable cached system prompt + volatile current-time context in user-message/tool-runtime context.
    • #10006/#16235: Codex/Responses caching still needs proper cache telemetry/routing/breakpoints.

Fix Action

Fixed

PR fix notes

PR #15872: fix: prevent stale timestamp perception by injecting current time per-turn

Description (problem / solution / changelog)

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:

LayerContentLocationFrequency
System promptSession started: <time> (timezone)FrozenOnce per session (cache-stable)
User messageCurrent time: <time>\nTimezone: <tz>DynamicEvery turn (cache-safe)

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_call context). The Hermes codebase itself documents this principle:

"Context is ALWAYS injected into the user message, never the system prompt. This preserves the prompt cache prefix — the system prompt stays identical across turns so cached tokens are reused."

User-message injection also composes naturally with the existing plugin system — time context merges with plugin context in _plugin_user_context.

Changes

  1. run_agent.py_build_system_prompt(): Rename "Conversation started""Session started" and add timezone name
  2. run_agent.pyrun_conversation(): Inject current time + timezone into _plugin_user_context on every turn
  3. run_agent.py_handle_max_iterations(): Inject current time into the summary request user message (recovery path)
  4. hermes_time.py: Add get_timezone_name() public API
  5. Comment update: Layer 6 comment now documents the split explicitly
  6. Tests: 4 new/updated tests covering cache safety, user-message injection, and max-iterations path

Test results

tests/run_agent/test_run_agent.py::TestBuildSystemPrompt::test_includes_datetime PASSED
tests/run_agent/test_run_agent.py::TestBuildSystemPrompt::test_excludes_current_time_from_cached_prompt PASSED
tests/run_agent/test_run_agent.py::TestHandleMaxIterations::test_summary_injects_current_time_into_user_message PASSED
tests/run_agent/test_run_agent.py::TestRunConversation::test_turn_level_time_injected_into_user_message PASSED

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.

Changed files

  • hermes_time.py (modified, +40/-0)
  • run_agent.py (modified, +48/-4)
  • tests/run_agent/test_run_agent.py (modified, +73/-2)
  • tests/run_agent/test_run_agent_codex_responses.py (modified, +31/-0)

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

Description (problem / solution / changelog)

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

<!-- Check the one that applies. -->
  • 🐛 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.pyBaseModalExecutionEnvironment._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.

Changed files

  • run_agent.py (modified, +11/-0)
  • tests/run_agent/test_run_agent.py (modified, +24/-0)
  • tests/tools/test_base_environment.py (modified, +72/-0)
  • tests/tools/test_local_env_blocklist.py (modified, +56/-0)
  • tests/tools/test_modal_utils_tz.py (added, +74/-0)
  • tools/environments/base.py (modified, +27/-0)
  • tools/environments/local.py (modified, +23/-1)
  • tools/environments/modal_utils.py (modified, +9/-0)

PR #8689: perf: stabilize system prompt timestamp across compression cycles

Description (problem / solution / changelog)

Summary

Stabilizes the "Conversation started" timestamp in the system prompt across compression cycles, preventing prefix cache invalidation on local LLM backends.

Before

now = _hermes_now()  # current time — changes after every compression!
timestamp_line = f"Conversation started: {now.strftime(...)}"

After

_start = getattr(self, "session_start", None) or _hermes_now()  # set once at __init__
timestamp_line = f"Conversation started: {_start.strftime(...)}"
# Only after compression:
timestamp_line += f"\nLast context compaction: {_now.strftime(...)} (#{count})"

Evidence

MITM proxy analysis showed the timestamp was the ONLY difference between system prompts. On LM Studio with phi-3.5-mini (~17K token prompt): 3.6x latency penalty per cache miss (2.2s vs 8.0s).

After fix, validated via MITM: "Conversation started" stays stable. Post-compression adds exactly one line, preserving the prefix before it for partial cache matching.

Test plan

  • MITM proxy validation: "Conversation started" stable across compression
  • Post-compression prompt adds "Last context compaction: {date} (#{count})"
  • Defensive: handles missing session_start and context_compressor gracefully
  • CI tests pass

Closes #8687. Relates to #3353, #4319.

Changed files

  • run_agent.py (modified, +11/-2)

PR #5713: feat(caching): multi-block system prompt with tiered TTLs (v2)

Description (problem / solution / changelog)

Summary

Refactor Anthropic prompt caching to use a structured multi-block system prompt with per-block cache_control markers instead of a single monolithic system message. This maximizes cache hits by isolating volatile content (timestamps, platform hints) from stable content (identity, skills, memory).

Architecture

The system prompt is now assembled as three SystemPromptBlock instances with different cache TTLs:

BlockTTLContents
static1hSoul.md / default identity, tool-aware guidance (memory, session_search, skills), Nous subscription prompt, tool-use enforcement, model-specific operational guidance (Google/OpenAI), skills system prompt
session5mCustom system_message, memory store blocks (memory + user), external memory provider block, context files (AGENTS.md/CLAUDE.md/etc.)
ephemeralnoneTimestamp + session/model/provider line, Alibaba identity workaround, platform hints

At API call time, blocks are converted to Anthropic content block format (`[{type: text, text: ..., cache_control: ...}, ...]`) and sent as the system message. Non-caching models fall through to the flat-string path unchanged.

New public API in `agent/prompt_caching.py`

  • `SystemPromptBlock`, `CacheMetrics`, `AggregatedCacheMetrics` dataclasses
  • `build_system_content_blocks(blocks)` — convert blocks to Anthropic format
  • `apply_anthropic_cache_control_v2(messages, tools, cache_ttl, native_anthropic)` — multi-block + tool caching with budget management (max 4 breakpoints across tools + system + messages)
  • `extract_cache_metrics(usage, api_mode)` — per-call cache extraction supporting both native Anthropic (`cache_read_input_tokens`, `cache_creation_input_tokens`) and OpenRouter (`prompt_tokens_details.cached_tokens`) response formats
  • `aggregate_cache_metrics(metrics_list)` — cross-turn aggregation

The v1 `apply_anthropic_cache_control` function and `_apply_cache_marker` helper are preserved unchanged for backward compatibility.

Integration in `run_agent.py`

  • New `_build_system_prompt_blocks()` method assembles the three tiered blocks and caches them on `self._cached_system_blocks`
  • The existing `_build_system_prompt()` method still returns a flat string (for backward compatibility with code paths that expect one) but now delegates to the block builder
  • Cached blocks are invalidated on context compression (`_cached_system_blocks = None` alongside `_cached_system_prompt = None`)
  • At API call time, when `_use_prompt_caching` is enabled and `_cached_system_blocks` is populated, a multi-block path builds `{role: system, content: [...]}` with cache_control markers already set per block
  • Plugin turn context (`_plugin_turn_context`) remains reserved for future system-level plugin instructions; plugin context from pre_llm_call hooks still goes into user messages (unchanged)
  • Fallback flat-string path handles non-caching models and pre-structured content correctly

Test coverage

  • `tests/agent/test_prompt_caching.py` — 46 unit tests covering v1 (preserved) and v2 functions: data structures, cache markers, content block conversion, pre-structured detection, breakpoint budgeting, metrics extraction and aggregation
  • `tests/agent/test_prompt_caching_v2.py` — 38 additional integration tests for v2 behavior (tool caching interaction with system blocks, budget with pre-structured content, backward compatibility with v1 code paths)
  • `tests/test_prompt_caching_integration.py` — 10 integration tests against `run_agent.py` block assembly (three-block structure, tier TTLs, timestamp in ephemeral block only, cache invalidation, backward-compat string return, non-caching models unaffected)

Verified: 317 tests passing (all of the above plus `tests/test_run_agent.py` regression suite).

Test plan

  • All new v2 unit tests pass (`pytest tests/agent/test_prompt_caching.py tests/agent/test_prompt_caching_v2.py`)
  • Integration tests against `run_agent.py` block assembly pass (`pytest tests/test_prompt_caching_integration.py`)
  • Full run_agent.py regression suite passes (`pytest tests/test_run_agent.py`)
  • `run_agent` imports cleanly
  • Manual: verify cache hit rate improves on a multi-turn conversation with stable context files (reviewer action)
  • Manual: verify non-caching models (e.g. local Ollama) still work via flat-string fallback (reviewer action)

Platforms tested

Linux (WSL2, Ubuntu 22.04), Python 3.11

🤖 Generated with Claude Code

Changed files

  • agent/prompt_caching.py (modified, +252/-6)
  • run_agent.py (modified, +169/-83)
  • tests/agent/test_prompt_caching.py (modified, +316/-0)
  • tests/agent/test_prompt_caching_v2.py (added, +469/-0)
  • tests/test_prompt_caching_integration.py (added, +209/-0)

PR #10006: fix: codex_responses prompt caching — session routing headers + cache_write_tokens field

Description (problem / solution / changelog)

Problem

Prompt caching does not work when using codex_responses API mode with OpenAI-compatible providers (e.g. theclawbay). Every request is a cache miss despite prompt_cache_key being set in the request body.

Root cause

Two issues:

1. Missing session routing headers (run_agent.py)

The OpenAI client is initialized without session_id or x-client-request-id headers for codex providers. These headers are required for server-side cache routing — they tell the backend to route requests to the same server that holds the cached prompt prefix.

The official Codex CLI sends these unconditionally. Hermes sets default_headers for OpenRouter, GitHub Copilot, Kimi, and Qwen — but never for Codex/theclawbay.

2. Wrong field name for cache_write_tokens (agent/usage_pricing.py)

The codex_responses branch reads cache_creation_tokens (Anthropic naming convention) instead of cache_write_tokens (OpenAI Responses API naming). This means cache write tokens are always reported as 0.

Fix

Patch 1: Session routing headers

After session_id is assigned during __init__, inject session_id and x-client-request-id into default_headers for codex_responses mode. Also applied in _apply_client_headers_for_base_url() so headers survive /model switches.

Patch 2: cache_write_tokens field

Read cache_write_tokens first (OpenAI naming), fall back to cache_creation_tokens for backward compatibility.

Tests

  • test_codex_responses_reads_cache_write_tokens_field — verifies correct field is read
  • test_codex_responses_falls_back_to_cache_creation_tokens — backward compat
  • test_codex_responses_injects_session_routing_headers — verifies headers are set

Affected files

FileChange
run_agent.pyInject session routing headers for codex_responses mode (+22 lines)
agent/usage_pricing.pyRead cache_write_tokens before cache_creation_tokens (+3/-1 lines)
tests/agent/test_usage_pricing.py2 new tests
tests/run_agent/test_run_agent.py1 new test

Changed files

  • agent/usage_pricing.py (modified, +3/-1)
  • run_agent.py (modified, +22/-0)
  • tests/agent/test_usage_pricing.py (modified, +36/-0)
  • tests/run_agent/test_run_agent.py (modified, +19/-0)

PR #10448: fix(agent): inject turn-level live time context

Description (problem / solution / changelog)

Summary

Hermes currently has a session-level Conversation started: ... timestamp, but it does not provide a reliable turn-level live sense of "now" for each API call.

This PR adds a turn-scoped Current time: YYYY-MM-DD HH:MM (TZ) hint at API-call time instead of baking live time into the cached session prompt.

What changed

  • add _build_live_time_context() in run_agent.py
  • add _build_turn_scoped_system_prompt() so live time is injected per API call, not into _build_system_prompt()
  • use that helper in both:
    • run_conversation()
    • _handle_max_iterations()
  • add targeted tests for:
    • cached system prompt stays free of Current time:
    • chat-completions run path injects live time
    • max-iteration summary path injects live time
    • codex responses path injects live time

Why this shape

This is intentionally aligned with the request in #10421:

  • live time is turn-scoped
  • it is injected at API-call time
  • it does not mutate the session-cached system prompt
  • it works for both chat-completions and codex-responses paths

Relation to other work

This PR is intentionally narrower than #10061.

  • #10061 focuses on timezone propagation to prompts and terminal environments
  • this PR only addresses turn-level live current-time injection

They are adjacent, but not the same fix.

Repro verified

I re-verified the gap locally on:

  • latest stable v0.9.0 released on April 13, 2026
  • current main at 422f2866 on April 16, 2026

In both cases, Hermes still had only the session-start timestamp and no turn-level live Current time: context.

How to test

python -m pytest tests/run_agent/test_run_agent.py -k 'turn_level_live_time or excludes_turn_level or summary_injects'
python -m pytest tests/run_agent/test_run_agent_codex_responses.py -k 'turn_level_live_time'

Refs #10421

Changed files

  • run_agent.py (modified, +23/-6)
  • tests/run_agent/test_run_agent.py (modified, +52/-0)
  • tests/run_agent/test_run_agent_codex_responses.py (modified, +24/-0)

PR #9784: feat(gateway): add optional inbound message timestamp prefix

Description (problem / solution / changelog)

What does this PR do?

<!-- Describe the change clearly. What problem does it solve? Why is this approach the right one? -->

This PR adds an optional configuration flag message_timestamp_prefix that prepends a compact timestamp to inbound user messages before they are passed to the agent.

When enabled, Hermes will prefix inbound messages like:

[04-14 23:47] hello

Related Issue

<!-- Link the issue this PR addresses. If no issue exists, consider creating one first. -->

Fixes #9628

Type of Change

<!-- Check the one that applies. -->
  • 🐛 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

<!-- List the specific changes. Include file paths for code changes. -->

Config support

Added new optional gateway config:

gateway.message_timestamp_prefix: true

Implemented:

  • config parsing support
  • nested gateway: YAML compatibility
  • serialization/deserialization handling

Files:

gateway/config.py

Runtime timestamp injection

Prepends timestamp during inbound message preprocessing:

gateway/run.py::_prepare_inbound_message_text()

Adds helper:

_format_inbound_message_timestamp()

Behavior:

  • format: [MM-DD HH:MM]
  • default timezone: UTC
  • override via:
HERMES_TIMEZONE=America/New_York

Gracefully falls back to UTC if timezone invalid.

Ordering guarantees

Preserves sender-prefix ordering in shared threads:

[timestamp] [username] message

Example:

[04-14 23:47] [alice] can you check this later?

Tests added

Added coverage for:

  • config parsing via nested gateway: dict
  • config.yaml loading bridge behavior
  • timestamp prefix formatting
  • ordering with username prefixes
  • UTC default handling

Files:

tests/gateway/test_stt_config.py

How to Test

<!-- Steps to verify this change works. For bugs: reproduction steps + proof that the fix works. -->
  1. Enable the feature in config:
gateway:
  message_timestamp_prefix: true
  1. Run Hermes normally
hermes
  1. Send a message from any platform

Expected:

[MM-DD HH:MM] your message

Optional timezone override:

export HERMES_TIMEZONE=America/New_York

Restart Hermes and verify updated timestamps.

Checklist

<!-- Complete these before requesting review. -->

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: <!-- e.g. Ubuntu 24.04, macOS 15.2, Windows 11 -->

Documentation & Housekeeping

<!-- Check all that apply. It's OK to check "N/A" if a category doesn't apply to your change. -->
  • [] I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • [] I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • [] I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

For New Skills

<!-- Only fill this out if you're adding a skill. Delete this section otherwise. -->
  • This skill is broadly useful to most users (if bundled) — see Contributing Guide
  • SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
  • No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
  • I've tested the skill end-to-end: hermes --toolsets skills -q "Use the X skill to do Y"

Screenshots / Logs

<!-- If applicable, add screenshots or log output showing the fix/feature in action. -->

Changed files

  • gateway/config.py (modified, +14/-0)
  • gateway/run.py (modified, +14/-0)
  • scripts/release.py (modified, +1/-0)
  • tests/gateway/test_stt_config.py (modified, +89/-1)

PR #5487: feat(gateway): inject current timestamp into user messages

Description (problem / solution / changelog)

Summary

Injects a human-readable timestamp at the top of every user message before it reaches the agent, so the agent always knows the current date and time without needing to invoke a tool call.

Format: [Current time: Monday, April 06, 2026 07:36 AM]

Why

Agents running long conversations often lose track of time. The only built-in mechanism is the Conversation started: header, which becomes stale after minutes or hours of back-and-forth. Currently the agent must spend a date tool call just to answer "what time is it?" or "what day is today?" — wasting tokens and latency.

This is especially important for:

  • Cron-scheduled tasks — the agent wakes up with no conversation history and no time context
  • Long multi-turn conversations — the original timestamp is hours old
  • Time-sensitive instructions — "remind me at 3pm", "is it past midnight?", etc.

What Changed

Core Injection (gateway/run.py)

Inside _run_agent(), right before agent.run_conversation():

  • Check display.gateway_timestamp config (default: true)
  • Get current time via datetime.now()
  • Set locale to en_US.UTF-8 for consistent day/month names (with graceful fallback)
  • Format as human-readable string: Monday, April 06, %Y %I:%M %p
  • Prepend [Current time: ...]\n\n to the message

Config Toggle (hermes_cli/config.py)

  • Add display.gateway_timestamp: true to DEFAULT_CONFIG
  • Persisted in config.yaml — survives restarts

Slash Command (hermes_cli/commands.py + gateway/run.py)

  • Add /timestamps (alias /ts) command — toggles injection on/off
  • Available in both CLI and messaging gateway
  • Takes effect immediately without restart
  • Persists to config.yaml

Design Decisions

DecisionRationale
datetime.now() instead of injecting into system promptThe message-level approach works for ALL model providers (some strip system prompts)
Locale en_US.UTF-8 with try/except fallbackServer locale may be non-English; we want consistent English output
Prepend to message, not modify agent APIZero-impact change — no changes to agent interface, prompt builder, or context files
Only in _run_agent (gateway path)CLI mode has direct terminal access; gateway agents need the injection
Config toggle + slash commandSome users may not want the token overhead; easy on/off without code changes
Default trueTime awareness is universally useful; users who don't want it can opt-out

Testing

Manual testing on self-hosted instance:

  • Agent correctly reports current time without any tool calls
  • Works across long multi-turn conversations (timestamp updated per message)
  • Cron jobs also receive timestamps (they pass through the same _run_agent path)
  • Gracefully handles missing en_US locale (falls back to server default)
  • /timestamps toggle works — ON/OFF states persist across messages and restarts
  • Config display.gateway_timestamp: false correctly disables injection

Changed files

  • gateway/run.py (modified, +76/-0)
  • hermes_cli/commands.py (modified, +2/-0)
  • hermes_cli/config.py (modified, +1/-0)

PR #17531: docs: document tool-time advisory-not-enforcement principle

Description (problem / solution / changelog)

Closes #17475.

Summary

Adds TOOL-PRINCIPLES.md at repo root capturing the architectural invariant established in #17459: runtime time/timezone/contact-window facts are surfaced to the agent as advisory context, not silently enforced in the control plane.

Why this doc needs to exist

Two closed-unmerged PRs tried to add quiet-hours enforcement and were rejected for the same reason:

  • #4711 feat: Add profile-scoped gateway autonomy runtime — held contacts in control plane
  • #7402 feat: drain Ockham outbox notifications via Hermes scheduler — held approvals during quiet hours

Without a durable written principle, it's easy for a future contributor to re-invent the same shape and get rejected again. The doc also names the preferred shape (#10421 / #15872: inject Current time + timezone into the ephemeral user-message context) so reviewers and contributors have a positive template to point at.

Shape

Each principle follows a consistent template so future invariants can be appended without re-designing the structure:

  1. Title — one-line statement of the invariant
  2. Decision — date + originating issue/PR references
  3. Why — failure mode the inverse produces
  4. What this means in practice — rejected vs encouraged PR shapes
  5. Related history — closed PRs demonstrating the wrong shape + open PRs doing it right

This first entry documents tool-time. Future candidates (credential-scope boundaries, cross-platform identity, etc.) can slot in identically.

Not in scope

  • No behavior change.
  • Does not close #10421, #15872, #17474 (those implement the principle; this just writes it down).
  • Does not take a position on whether opt-in quiet-hours enforcement should exist as a gateway plugin — the doc explicitly leaves that door open if visibility/overridability constraints are met.

File

  • TOOL-PRINCIPLES.md (new, 88 lines)

Changed files

  • TOOL-PRINCIPLES.md (added, +88/-0)

PR #17548: feat(tools): tool-runtime time advisory context helper

Description (problem / solution / changelog)

Closes #17474 (helper layer). Follow-up to #17459 and the principle documented in #17531 TOOL-PRINCIPLES.md §1.

Summary

Adds tools/tool_runtime_time_context.py: a shared helper that resolves the user's local time, timezone, and configured contact window, and surfaces them as structured context a time-sensitive tool (email send, calendar create, agent mailbox, gateway delivery, scheduler) can include in its return JSON.

Advisory only. Hermes core never silently blocks, delays, or rewrites tool calls based on the contact window. The agent sees the facts and decides. This is the positive shape that TOOL-PRINCIPLES.md §1 explicitly names, in contrast to the control-plane-enforcement shape in closed-unmerged #4711 / #7402.

Surface

from tools.tool_runtime_time_context import build_tool_runtime_time_context

ctx = build_tool_runtime_time_context()
result = {"sent": True, "time_context": ctx.to_dict()}

Returned dict (matches the issue's proposed shape):

{
  "now": "2026-04-29T02:14:00-07:00",
  "user_timezone": "America/Los_Angeles",
  "contact_window": ["08:00", "23:00"],
  "within_contact_window": false,
  "advisory": "It is 02:14 in the user's timezone (America/Los_Angeles). This is outside their configured contact window (08:00–23:00). Consider asking or deferring unless urgent."
}

Config

All optional, top-level in config.yaml:

tool_runtime:
  user_timezone: "America/Los_Angeles"   # IANA TZ; falls back to system local
  contact_window:
    start: "08:00"                       # HH:MM
    end:   "23:00"                       # HH:MM; end is exclusive

Defaults to 08:00–23:00 in the system local timezone when neither is configured. HERMES_USER_TIMEZONE env var overrides user_timezone. Wrap-around windows (22:00 → 06:00) supported. Malformed values fall back to defaults rather than raising — a bad config entry must never break a tool call on the hot path.

Not in scope (follow-ups)

This PR intentionally ships the helper + tests only. Per-tool integrations land as separate PRs so each can be reviewed on its own merits against its specific return shape:

  • email send tools
  • calendar create/update tools
  • agent mailbox / inbox/outbox tools
  • send_message / gateway delivery tools
  • wait / scheduler / cron / autonomy-watch tools

Follow-up checklist tracked in #17474.

Tests

11 tests under tests/tools/test_tool_runtime_time_context.py:

  • HH:MM parser (valid + every class of invalid)
  • _within_window normal hours / crosses midnight / zero-width
  • default config inside + outside window
  • configured user timezone shifts the clock correctly
  • custom contact window
  • wrap-around window at night
  • unknown TZ falls back silently to system local
  • naive datetime rejected with ValueError
  • to_dict() returns json-serializable shape (list, not tuple, for contact_window)
  • malformed window entries fall back to defaults

Files

  • tools/tool_runtime_time_context.py — new, 185 lines
  • tests/tools/test_tool_runtime_time_context.py — new, 196 lines

Changed files

  • tests/tools/test_tool_runtime_time_context.py (added, +182/-0)
  • tools/tool_runtime_time_context.py (added, +199/-0)

Code Example

Runtime time context:
- Current time: 2026-04-29 08:34:12 America/Los_Angeles (UTC-07:00)
- User timezone: America/Los_Angeles
- Session started: 2026-04-29 08:00:02 America/Los_Angeles

---

ToolRuntimeTimeContext(
    now=<aware datetime>,
    user_timezone="America/Los_Angeles",
    contact_window=("08:00", "23:00"),
    within_contact_window=True | False,
    advisory="It is 02:14 in the user's timezone; this may be outside their normal contact window. Ask/consider deferring unless urgent."
)
RAW_BUFFERClick to expand / collapse

Context / regression

Quiet hours should not be a control-plane policy that silently withholds or delays agent actions. Per the tool-use principles we want the agent to see the relevant runtime facts and make/justify the decision, not have a background subsystem enforce user-contact policy invisibly.

The current direction is fragmented:

  • Closed, not merged / should not be revived as-is
    • #4711 feat: Add profile-scoped gateway autonomy runtime introduced gateway autonomy with quiet-hours controls in the control plane. It is closed and unmerged; do not resurrect the quiet-hours enforcement shape.
    • #7402 feat: drain Ockham outbox notifications via Hermes scheduler was closed and unmerged; it explicitly held approvals/digests during quiet hours. That is the wrong semantic for Hermes core unless the agent is told and chooses deferral.
  • Open time-awareness work to salvage / consolidate
    • #10421 captures the core need: turn-level live current time awareness.
    • #15872 is the preferred implementation shape: inject Current time + timezone into the user-message/ephemeral context path, preserving prompt-cache stability.
    • #10448 and #5241 address similar bugs but put live time into the system-prompt path; that should be avoided because it undermines prompt caching.
    • #10061 is adjacent and useful: propagate configured timezone to agent prompt/terminal environments.
    • #9784/#5487 are gateway-only timestamp-prefix variants; we should avoid multiple parallel timestamp mechanisms and consolidate on one core ephemeral context path.
  • Open caching regressions related to this work
    • #17335: get_tool_definitions() quiet-mode cache pollution causes duplicate LCM schemas in Gateway. This should be treated as an immediate bug fix.
    • #15866/#8687/#8689/#5713: system-prompt timestamp / prompt-cache invalidation concerns. The durable direction is stable cached system prompt + volatile current-time context in user-message/tool-runtime context.
    • #10006/#16235: Codex/Responses caching still needs proper cache telemetry/routing/breakpoints.

Goal

Move from control-plane quiet-hours enforcement to agent-visible, timezone-aware tool context:

  1. Fix the caching regression(s).
  2. Give the agent reliable current time + user timezone context on every turn and on relevant tool calls.
  3. For time-sensitive tools (email, calendars, agent mailbox, send/wait/scheduler-style tools, future autonomy/watch tools), surface a clear advisory when the action appears outside the user's normal contact window — but do not block or delay it in the control plane.

Proposed design

1. Stable prompt cache, live time outside the cached system prompt

  • Keep the cached system prompt byte-stable across turns.
  • Replace the ambiguous Conversation started: mental model with a stable session-start anchor only.
  • Inject volatile time context through the existing ephemeral user-message context path, e.g.
Runtime time context:
- Current time: 2026-04-29 08:34:12 America/Los_Angeles (UTC-07:00)
- User timezone: America/Los_Angeles
- Session started: 2026-04-29 08:00:02 America/Los_Angeles
  • Prefer/salvage #15872's approach over per-turn system-prompt mutation.

2. Tool-runtime time advisory, not enforcement

Add a small shared helper/context object available to tool handlers, for example:

ToolRuntimeTimeContext(
    now=<aware datetime>,
    user_timezone="America/Los_Angeles",
    contact_window=("08:00", "23:00"),
    within_contact_window=True | False,
    advisory="It is 02:14 in the user's timezone; this may be outside their normal contact window. Ask/consider deferring unless urgent."
)

Tools may include this in their returned JSON/result text when relevant. The control plane must not silently suppress, defer, or rewrite the action.

Examples:

  • wait / process.wait / future agent-mailbox wait:
    • Return current local time and deadline/elapsed-time interpretation.
    • If waiting would cross the user's normal contact window, warn the agent.
  • Email/calendar/mailbox tools:
    • Include current_time, user_timezone, and contact_window_advisory in reads/searches and before sends/creates where relevant.
  • send_message / proactive delivery paths:
    • Warn if sending outside the window; do not block. The agent can explicitly say why it is sending now or choose to schedule/defer.

3. Remove/avoid control-plane quiet-hours semantics

  • No core gateway/scheduler/autonomy subsystem should silently hold messages merely because of quiet hours.
  • If a subsystem keeps a user-configured contact window, it should expose that as agent context/advisory only.
  • Any future hard block must be a separate, explicit user-configured safety setting with visible tool feedback, not the default quiet-hours behavior.

Implementation slices

  1. Immediate cache correctness

    • Fix #17335: defensive-copy cached tool definition lists and dedupe context-engine tool injection.
    • Add regression tests for Gateway/quiet-mode repeated agent creation with LCM/context-engine tools.
  2. Core time context consolidation

    • Salvage #15872: inject current time/timezone into ephemeral user-message context per turn.
    • Keep system prompt cache-stable; update tests to prove Current time: is not in _build_system_prompt().
    • Incorporate timezone helper pieces from #10061 where still applicable.
    • Close/supersede #10448/#5241/#9784/#5487-style duplicate mechanisms once consolidated.
  3. Tool runtime advisory helper

    • Add a shared helper for aware now, resolved user timezone, configurable advisory window, and advisory text.
    • Wire it into time-sensitive tools first: wait/process-wait, email/calendar/agent-mailbox if present, send/proactive-delivery surfaces.
    • Ensure tool output is advisory and visible to the model.
  4. Docs / principles alignment

    • Document: "time policy is surfaced to the agent; control plane does not silently enforce quiet hours."
    • Add tests that verify no quiet-hours path silently suppresses delivery/action without returning an advisory to the agent.

Acceptance criteria

  • get_tool_definitions(quiet_mode=True) cache pollution is fixed; repeated Gateway agents do not accumulate duplicate tool schemas.
  • Current time + user timezone are available on every agent turn without mutating the cached system prompt.
  • Prompt-cache stability tests prove volatile time context is excluded from the cached system prompt.
  • Time-sensitive tools return explicit time/timezone/advisory metadata when appropriate.
  • Quiet-hours/contact-window behavior is advisory by default and visible to the agent; no default control-plane suppression/delay.
  • Duplicate timestamp-prefix/current-time PRs are either closed as superseded or rebased into the chosen core path.
  • Docs explain the principle and the intended extension point for future tools.

Non-goals

  • Building a full policy engine.
  • Adding a broad autonomy control plane.
  • Hard-blocking sends/actions outside contact windows by default.
  • Moving volatile time data into the system prompt.

Suggested priority

P1. This blocks reliable gateway use because caching/schema pollution is already user-visible (#17335), and the quiet-hours direction is an architectural regression if it lands as hidden control-plane enforcement.

extent analysis

TL;DR

Fix the caching regression and implement agent-visible, timezone-aware tool context to replace control-plane quiet-hours enforcement.

Guidance

  1. Fix caching regression: Address #17335 by defensive-copying cached tool definition lists and deduplicating context-engine tool injection.
  2. Implement core time context consolidation: Salvage #15872 to inject current time and timezone into the ephemeral user-message context per turn, keeping the system prompt cache-stable.
  3. Create a tool runtime advisory helper: Add a shared helper for aware now, resolved user timezone, configurable advisory window, and advisory text, and wire it into time-sensitive tools.
  4. Verify prompt-cache stability: Update tests to prove Current time: is not in _build_system_prompt() and ensure tool output is advisory and visible to the model.

Example

ToolRuntimeTimeContext(
    now=<aware datetime>,
    user_timezone="America/Los_Angeles",
    contact_window=("08:00", "23:00"),
    within_contact_window=True | False,
    advisory="It is 02:14 in the user's timezone; this may be outside their normal contact window. Ask/consider deferring unless urgent."
)

Notes

The provided guidance focuses on the immediate caching regression fix and the implementation of the core time context consolidation, as these are critical steps towards replacing control-plane quiet-hours enforcement with agent-visible, timezone-aware tool context.

Recommendation

Apply the workaround by fixing the caching regression and implementing the core time context consolidation, as this approach aligns with the goal of providing agent-visible, timezone-aware tool context and avoids control-plane quiet-hours enforcement.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix Rework quiet-hours/time awareness: surface time to agent/tools, don't enforce in control plane [10 pull requests, 1 comments, 1 participants]