Skip to content

feat(gateway): add optional message timestamps for LLM context - #41633

Closed
WolframRavenwolf wants to merge 1 commit into
NousResearch:mainfrom
WolframRavenwolf:feat/gateway-message-timestamps
Closed

feat(gateway): add optional message timestamps for LLM context#41633
WolframRavenwolf wants to merge 1 commit into
NousResearch:mainfrom
WolframRavenwolf:feat/gateway-message-timestamps

Conversation

@WolframRavenwolf

Copy link
Copy Markdown
Contributor

Summary

Adds optional, stable gateway user-message timestamps for LLM context.

When enabled with:

gateway:
  message_timestamps:
    enabled: true

Hermes renders user messages in model context as human-readable timestamped lines while keeping persisted transcript content clean.

Motivation

Messaging-platform agents often need temporal context: whether a message was sent minutes ago, overnight, or during a prior travel/workflow window can materially affect the answer. The previous naive approach of prefixing message text directly can contaminate persisted transcripts and duplicate timestamps on replay.

This PR adds a safer path:

  • store/parse timestamp metadata separately from text where possible
  • render a single timestamp prefix only for model context
  • strip legacy duplicate timestamp prefixes defensively
  • preserve old upstream behavior by default unless users opt in

Verification

  • python3 -m py_compile gateway/message_timestamps.py gateway/run.py gateway/session.py hermes_state.py run_agent.py agent/agent_init.py agent/conversation_loop.py tests/gateway/test_message_timestamps.py
  • uv run pytest tests/gateway/test_message_timestamps.py tests/test_hermes_state.py::TestMessageStorage -q -o addopts=

Result: 36 passed.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Jun 8, 2026
@WolframRavenwolf
WolframRavenwolf force-pushed the feat/gateway-message-timestamps branch 4 times, most recently from a5b00a1 to 359d72d Compare June 15, 2026 22:41
Consolidates these related Amy fork patches:
- 429830f39 feat(gateway): inject message timestamps into user messages for LLM context
- 3c3d6fac0 fix: handle both ISO string and epoch float timestamps in history replay
- 2874f7725 feat: human-friendly timestamp format with weekday and timezone name
- 3735f4c8b fix: render gateway message timestamps once
@WolframRavenwolf
WolframRavenwolf force-pushed the feat/gateway-message-timestamps branch from 359d72d to ad74056 Compare June 16, 2026 00:38
teknium1 added a commit that referenced this pull request Jun 16, 2026
Follow-up to salvaged PR #41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
teknium1 added a commit that referenced this pull request Jun 16, 2026
Follow-up to salvaged PR #41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #47253 — your commit was cherry-picked onto current main with your authorship preserved in git log (rebase-merge).

The timestamp metadata storage, the clean-transcript handling, and the defensive de-duplication on replay are all in. One change on top: the in-context render is now gated behind gateway.message_timestamps.enabled (default off) to match the opt-in behavior the PR described — the previous version injected the prefix unconditionally for every gateway user. Timestamp metadata is still captured + persisted regardless of the toggle, so enabling it later also surfaces send-times for past messages. Added the DEFAULT_CONFIG entry, docs, and gate tests. Thanks!

@teknium1 teknium1 closed this Jun 16, 2026
jhjaggars-hermes added a commit to jhjaggars/hermes-agent that referenced this pull request Jun 17, 2026
* feat: add z-ai/glm-5.2 to OpenRouter and Nous model lists

Z.ai released GLM 5.2 on 2026-06-15, available on OpenRouter:
  - https://openrouter.ai/z-ai/glm-5.2

GLM-5.2 is Z.ai's flagship for long-horizon tasks, shipping a 1M-token
context window (up from 200K on GLM 5.1) and tool calling. Per the
OpenRouter API: text-only, context_length 1048576, tools supported.
No separate -fast variant exists.

The 1M context length, native zai picker entry, setup wizard, and Z.ai
coding-plan auth entries for glm-5.2 already landed on main. This fills
the remaining gap: the two aggregator surfaces where glm-5.1 appears but
glm-5.2 did not.

Changes:

  hermes_cli/models.py
    - Add z-ai/glm-5.2 to the OpenRouter fallback snapshot (OPENROUTER_MODELS)
      and the Nous Portal curated list (_PROVIDER_MODELS["nous"]), newest
      flagship first. Live catalogs surface it automatically when reachable;
      the fallback lists matter when the manifest fetch fails.

  website/static/api/model-catalog.json
    - Regenerated via scripts/build_model_catalog.py (not hand-edited) so the
      manifest stays in sync with the source lists; guarded by
      tests/hermes_cli/test_model_catalog.py.

* feat(prompt): make context-file truncation limit configurable

PROBLEM: Automatic context files such as SOUL.md and AGENTS.md were capped by a hardcoded CONTEXT_FILE_MAX_CHARS value. Amy's local fork had raised that constant from 20K to 25K so a larger SOUL.md would not be silently truncated, but the hardcoded 25K value changed upstream default behavior and made the patch less generally useful.

SOLUTION: Restore the upstream-compatible 20K default, add a context_file_max_chars config setting for users who intentionally keep larger identity/project-context files, keep chat-visible truncation warnings, and document the new setting. Tests cover the default, config override, explicit max_chars precedence, and the warning text.

* fix(prompt): isolate truncation warnings per context

Follow-up to salvaged PR NousResearch#41619: replace the module-global
_truncation_warnings list with a contextvars.ContextVar so concurrent
gateway-session prompt builds can't drain or clear each other's pending
warnings (cross-session leak). Adds a context-isolation test.

* feat(desktop): stream subagent activity into watch windows (NousResearch#47060)

* feat(desktop): stream subagent replies into watch windows

A desktop watch window resumes a child session lazily (no full agent) and
mirrors the parent-relayed `subagent.*` events into native child-session
stream events. The child's streamed reply text was never relayed, so the
window sat blank while the subagent "talked".

- delegate_tool: forward the child's `run_conversation` stream tokens up the
  progress relay as `subagent.text` (inert under CLI/TUI — their progress
  handlers ignore non-tool event types; only a gateway watch window mirrors it).
- server: mirror `subagent.text` -> `message.delta` on the child sid only, and
  skip the parent emit (per-token frames are meaningless on the parent session,
  which shows the child via the spawn tree). Demote `subagent.start` to a
  one-time goal header and drop the noisy `subagent.progress` mirror — tools
  already mirror natively.
- server: guard `_start_agent_build` so a lazy watch session spectating an
  in-flight child stays lazy; incidental RPCs were upgrading it to a full
  agent mid-stream and silently killing the mirror.

* fix(desktop): keep watch-window chat clear of titlebar chrome

Secondary windows (new-session scratch, subagent watch, cmd-click pop-out)
hide the titlebar tool cluster + session header, so the transcript ran to the
window's top edge and streamed text slid up under the OS traffic lights.

- Gate the hidden chrome on `isSecondaryWindow()` everywhere (app-shell,
  chat header, thread list) instead of the narrower new-session flag.
- Add a fixed opaque drag-strip at the top of the secondary-window transcript:
  content padding alone scrolls away with the text, so the strip masks
  anything behind it and keeps the window draggable like the main header.

* fix: WSL subagent window

* fix: subagent window top padding

---------

Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>

* feat(hooks): session:compress event_callback for MemPalace sync

* test(hooks): cover session:compress event; drop dead import

Follow-up to salvaged PR NousResearch#41624:
- Remove stray urllib.parse import in run_agent.py (cherry-pick cruft, unused)
- Add tests: session:compress emits with correct context, no-callback is
  safe, and a callback exception does not break compression

* feat(xai): default to grok-build-0.1

Switch the default model for the xAI/Grok provider and the xAI web
search backend from grok-4.3 to grok-build-0.1. grok-build-0.1 is
already recognized by the model metadata, so no new model definition
is required; grok-4.3 remains selectable.

* docs(xai): update default model references to grok-build-0.1

Reflect the default-model change in the xAI Grok OAuth guide, the web
search docs (EN + zh-Hans), and the web provider docstring. grok-4.3 is
kept in the model tables as the previous default; the Nous/OpenRouter
aggregator catalog still lists grok-4.3 and is left unchanged.

* fix(inventory): keep user-defined custom providers in model dedup

The NousResearch#45954 model-dedup builds `user_models` from every is_user_defined
row, then strips those model IDs from every row where is_aggregator(slug)
is True. But is_aggregator() returns True for *every* `custom:*` slug, and
list_authenticated_providers emits named custom providers with slug
`custom:<name>` and is_user_defined=True. So a user's own custom provider
is treated as an aggregator and filtered against user_models — which holds
exactly its own models (the row helped build that set). Every model is
removed, the row drops to zero, and the provider disappears from the model
picker.

Guard the dedup loop to skip is_user_defined rows: a user's configured
provider is never an aggregator duplicate of itself. Built-in aggregators
(openrouter, etc.) are still deduped as before. Adds a regression test.

* chore(release): map cyb0rgk1tty noreply email for AUTHOR_MAP

Salvage follow-up for PR NousResearch#46921 — CI matches contributor authorship on the
commit email, which is the GitHub noreply form.

* fix(telegram): resolve replies to rich (sendRichMessage) messages

Telegram does not echo a sendRichMessage's content back in
reply_to_message (.text/.caption empty, .api_kwargs None), so replies
to rich sends (briefings, the gateway's own rich finals) arrived with
no quotable text and the [Replying to: ...] injection was skipped.

Remember message_id -> text at send time in a best-effort JSON index
(gateway/rich_sent_store.py), and recover it on inbound when text and
caption are both empty. Best-effort and no-throw throughout: any
failure degrades to prior behavior and never breaks a send or message.

Salvaged from NousResearch#47375 by @x1erra. Dropped the cross-platform run.py
reply-prefix rewrite (out of scope; bloated every reply on every
platform) and scrubbed a docstring reference to an out-of-repo script.
Kept the inbound reply_to logging enrichment used to verify the fix.

* chore(release): AUTHOR_MAP entry for x1erra (Sierra)

* test(telegram): rich-reply recovery via send-time index

Cover NousResearch#47375 fix: record-on-rich-send + lookup-on-reply round trip,
lookup miss leaving reply_to_text None, and precedence (native quote
and echoed caption both win over the index fallback).

* fix(model-switch): probe /v1/models for providers without api_key

Section 3 of list_authenticated_providers (user-defined endpoints from
the providers: config section) required an api_key before probing the
endpoint's /v1/models for live model discovery. This broke local
self-hosted backends (llama.cpp, Ollama, vLLM, etc.) that don't require
authentication — they would only ever show the single default_model
from config instead of the full model catalog.

Section 4 (custom_providers list) already handled this correctly with
the policy: probe when api_key is set OR when no explicit models are
configured. Apply the same logic to Section 3 so local backends get
full model discovery without requiring a placeholder api_key workaround.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(model-switch): cover section-3 no-auth probe; map chimpera author

Salvage follow-up for PR NousResearch#29575: add regression tests for the section-3
no-api_key /v1/models probe (probes bare endpoints, skips when explicit
models set) and add the contributor AUTHOR_MAP entry.

* fix(skills): ignore support docs in skill discovery

Support files under references/, templates/, assets/, and scripts/ are progressive-disclosure data loaded through skill_view(..., file_path=...). They should not be treated as standalone skills during discovery or collision checks.

This prevents archived skill packages or support markdown files inside a real skill from shadowing active skills with the same name while still allowing top-level categories named scripts/templates/assets/references.

Tests cover:
- pruning nested SKILL.md files inside skill support directories
- preserving support-named top-level categories
- avoiding skill_view collisions from support markdown
- keeping archived package SKILL.md files accessible only through file_path

* fix(models): pass model.base_url to fetch_models in /model picker

The /model interactive picker resolved a base_url from user credentials
but never passed it to ProviderProfile.fetch_models(), causing the
picker to always query the provider's hardcoded default endpoint
instead of the user's custom URL (e.g. a company litellm proxy).

- providers/base.py: add optional base_url parameter to fetch_models()
- hermes_cli/models.py: pass resolved base_url to fetch_models()
- Update all subclass overrides for signature compatibility
- Add 6 regression tests covering override, fallback, and integration

* test(model-picker): cover two overlapping user-defined custom providers

Guards that two user-defined custom endpoints exposing an overlapping
model each keep their full catalog — the dedup must never cross-filter
two user-defined rows against each other.

* fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266) (NousResearch#47276)

* fix(desktop): re-download Electron binary via mirror when pack fails (NousResearch#47266)

Since NousResearch#38673 pinned build.electronDist to node_modules/electron/dist,
electron-builder reads the Electron binary straight from there and never
downloads it during `npm run pack`. That dist tree is only produced by the
electron package's postinstall (install.js) during `npm ci`. When that
download is blocked or throttled (GitHub's release host is unreachable in
some regions), the dist is missing and the build dies with:

    The specified electronDist does not exist: .../node_modules/electron/dist

The existing ELECTRON_MIRROR fallback in all three desktop-build paths
(scripts/install.ps1, scripts/install.sh, and `hermes desktop` in
hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but
pack never downloads Electron anymore, so the mirror was never used and the
retry re-read the same missing dist. The fallback was effectively dead.

Drive the mirror through electron's own downloader instead:

- Add a dist-presence check + a downloader helper (Test-ElectronDist /
  Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist,
  _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist
  + the path.txt version marker (electron's install.js short-circuits on it)
  and re-runs `node install.js`, optionally via a mirror.
- On the first retry, repopulate a missing dist from the canonical source;
  on the mirror retry, re-fetch through npmmirror.com, then pack.
- Gate the re-download on the dist check so an unrelated build failure
  (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final
  pack when the binary still can't be fetched instead of failing the same way.

* test(desktop): cover Electron dist re-download mirror fallback (NousResearch#47266)

Add behavior coverage for the electronDist re-download fix:

- _electron_dist_ok across linux/win32/darwin, including the partial-dist
  case (dir present but binary missing) that makes the pinned electronDist
  fail.
- _redownload_electron_dist: no-op when the binary is present, bail when
  install.js is absent, wipe a stale dist + path.txt marker and run
  electron's downloader with ELECTRON_MIRROR injected, and report failure
  when the download still produces no binary.
- `hermes desktop`: the mirror fallback now drives electron's own downloader
  before re-running pack, and skips the final pack entirely when the binary
  can't be fetched.

Replaces the old mirror test that asserted the (now-fixed) dead behavior of
re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads
Electron under the pinned electronDist, so that retry could never help.

* fix(desktop): honor pre-session model pick + restore global reasoning/speed defaults (NousResearch#47447)

* fix(desktop): keep the pre-session model pick selected in the picker

The composer picker derived its "current" row from `model.options ?? store`,
so model.options always won. Pre-session that query returns the PROFILE
DEFAULT, not the sticky composer pick — so selecting a model before a session
exists left the checkmark (and the picker's "current" line) on the default,
making the pick look ignored even though the pill updated.

Add `currentPickerSelection()`: with a live session the gateway's model.options
is authoritative; pre-session the sticky `$currentModel`/`$currentProvider`
wins, falling back to options. Wire it into ModelMenuPanel and ModelPickerDialog.

* feat(desktop): global reasoning/speed defaults in Settings → Model

The composer picker is now sticky-UI/per-session only and never writes the
profile default (NousResearch#46959), but Settings → Model had no reasoning/speed control
and `agent.reasoning_effort` wasn't in the curated config surface at all
(`service_tier` was buried in Advanced) — so there was nowhere to set the
profile default that crons/subagents/messaging resolve from.

Add capability-gated Reasoning (effort) + Fast controls beside the main model,
gated by the applied model's reported capabilities (reasoning defaults on, fast
off when unreported — same as the composer). They read/write `agent.reasoning_effort`
and `agent.service_tier` by round-tripping the config record, matching the
gateway's value semantics (service_tier "fast"/"priority"/"on" ⇒ fast).

* refactor(desktop): don't open the reasoning select from its row label

A <label> wrapping the Select forwarded text clicks to the trigger, opening
the dropdown unexpectedly. Plain row for reasoning; Fast stays a <label> so
clicking its text toggles the switch (expected for a checkbox-like control).

* feat(gateway): inject stable human-readable message timestamps

Consolidates these related Amy fork patches:
- 429830f39 feat(gateway): inject message timestamps into user messages for LLM context
- 3c3d6fac0 fix: handle both ISO string and epoch float timestamps in history replay
- 2874f7725 feat: human-friendly timestamp format with weekday and timezone name
- 3735f4c8b fix: render gateway message timestamps once

* feat(gateway): gate message timestamps behind opt-in (default off)

Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.

* Sync homelab/main to upstream/main (17251e8) with minimal carried patches

Rebased onto current upstream/main and reapplied the minimal homelab
patch set:
- Dockerfile: add iproute2 + GitHub CLI (gh) from official apt repo
- pyproject.toml: add langfuse optional extra
- plugins/observability/langfuse/__init__.py: Responses API serialization
- plugins/platforms/discord/adapter.py: role-mention invocation support
- tests/gateway/test_discord_role_mentions.py: role-mention test coverage
- tests/plugins/test_langfuse_plugin.py: Responses API test coverage
- .github/workflows/build.yml: GHCR image publish workflow

Upstream changes since last sync (9d2ec8d -> 17251e8):
- 35 commits from NousResearch/hermes-agent:main
- Desktop: tighten composer model picker, open new sessions in compact windows
- fix(skills): guard recursive skill delete against tree-escape
- fix(delegation): forward background flag in delegate_task dispatch
- fix(desktop): route global remote profile REST calls, sync sessions across windows
- fix(desktop): open remote-gateway artifacts via authenticated download
- fix(models): keep curated-first ordering in live+curated merge
- fix(memory): strip skill scaffolding for all providers
- feat(skills): replace shop-app with CLI-based shop skill
- And more (see upstream log)

Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>

---------

Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
Co-authored-by: Wolfram Ravenwolf <github.com@wolfram.ravenwolf.de>
Co-authored-by: teknium <127238744+teknium1@users.noreply.github.com>
Co-authored-by: brooklyn! <brooklyn.bb.nicholson@gmail.com>
Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com>
Co-authored-by: cyb0rgk1tty <214562553+cyb0rgk1tty@users.noreply.github.com>
Co-authored-by: Sierra (Hermes Agent) <stevenn.damatoo@gmail.com>
Co-authored-by: chimpera <11052595+chimpera@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: xxxigm <54813621+xxxigm@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
alanbratu pushed a commit to alanbratu/hermes-agent that referenced this pull request Jun 17, 2026
Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
al3xar pushed a commit to al3xar/hermes-agent that referenced this pull request Jun 17, 2026
Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
Follow-up to salvaged PR NousResearch#41633: the timestamp prefix injection was
unconditional. Gate the in-context render behind
gateway.message_timestamps.enabled (default false) at both the live-message
and history-replay sites; timestamp metadata is still captured + persisted
regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry,
docs, and gate tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint 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.

3 participants