Merge pull request #6965 from nesquena/release/stage-6947-1786567873

Release: guard persist_user_timestamp kwarg for older-agent compat (#6935)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-08-12 14:02:26 -07:00
commit 5defcfedfe
3 files changed
+79 -3

No files matched your search

+1
View File
@@ -5,6 +5,7 @@
### Fixed
- **WebUI no longer crashes a send when running against an older hermes-agent.** The streaming path passed `persist_user_timestamp=` to `agent.run_conversation()` unconditionally, so a WebUI paired with an older agent whose `run_conversation()` predates that keyword raised `TypeError: unexpected keyword argument` and the message failed. The three call sites now check the callable's signature first (via a small `_supports_kwarg` introspection helper) and only pass the timestamp when the agent accepts it — the same defensive pattern already used for `moa_config`. Modern agents are unaffected (the timestamp is still passed with the identical value); older agents degrade gracefully instead of erroring. Thanks @harsh2hell. (#6935)
- **`hermes sessions list` no longer shows blank titles for WebUI sessions.** Background title generation wrote the auto-title to the WebUI session sidecar but never to hermes-agent's `state.db`, so the CLI/TUI session list showed those WebUI sessions untitled. The title is now bridged into `state.db` after it's generated (and on adaptive refresh) via `set_auto_title_if_empty`, which respects title provenance — a name you set manually via CLI/Gateway/TUI (`user` source) is never overwritten by the auto-title, an already-auto-titled row stays put, and an existing session's `source` is preserved (the sync creates the row with INSERT-OR-IGNORE semantics). Thanks @liuguangyong93. (#6892)
- **No more phantom "Compressing context" barrier after switching sessions.** The compression UI state is per-session, but `loadSession()` did not clear it on a switch, so a compression card from a prior session could leak across the load and appear as a phantom "Compressing context" barrier on a fresh session that never triggered compression. Switching sessions now clears the compression UI (state, elapsed timer, composer session-lock, and barrier element) alongside the other per-session resets, before the transcript loads. A compression genuinely running on the session you switch *to* is unaffected — it re-surfaces from that session's own live event stream (and manual compression from its server job status). Thanks @happy5318. (#6938)
+20 -3
View File
@@ -21,6 +21,7 @@ import threading
import time
import traceback
import copy
import inspect
from pathlib import Path
from typing import Optional
@@ -307,6 +308,19 @@ def _install_streaming_cronjob_profile_wrapper() -> None:
_STREAMING_CRONJOB_WRAPPER_INSTALLED = True
def _supports_kwarg(func, kwarg_name: str) -> bool:
"""Return True if callable `func` accepts `kwarg_name` (explicitly or via **kwargs)."""
try:
sig = inspect.signature(func)
for param in sig.parameters.values():
if param.kind == param.VAR_KEYWORD or param.name == kwarg_name:
return True
return False
except Exception:
return True
_PERSISTENT_MEMORY_FILES = (
("memory", ("memories", "MEMORY.md")),
("user", ("memories", "USER.md")),
@@ -9896,8 +9910,9 @@ def _run_agent_streaming(
),
task_id=session_id,
persist_user_message=msg_text,
persist_user_timestamp=getattr(s, 'pending_started_at', None),
)
if _supports_kwarg(agent.run_conversation, 'persist_user_timestamp'):
_run_conversation_kwargs['persist_user_timestamp'] = getattr(s, 'pending_started_at', None)
# Only pass moa_config when a /moa override is actually active, so a
# normal send never trips a TypeError on an older hermes-agent whose
# run_conversation() predates the moa_config kwarg.
@@ -10399,8 +10414,9 @@ def _run_agent_streaming(
),
task_id=session_id,
persist_user_message=msg_text,
persist_user_timestamp=getattr(s, 'pending_started_at', None),
)
if _supports_kwarg(agent.run_conversation, 'persist_user_timestamp'):
_heal_kwargs['persist_user_timestamp'] = getattr(s, 'pending_started_at', None)
if moa_config is not None:
_heal_kwargs["moa_config"] = moa_config
_heal_result = agent.run_conversation(**_heal_kwargs)
@@ -11639,8 +11655,9 @@ def _run_agent_streaming(
),
task_id=session_id,
persist_user_message=msg_text,
persist_user_timestamp=getattr(s, 'pending_started_at', None),
)
if _supports_kwarg(_heal_agent.run_conversation, 'persist_user_timestamp'):
_heal_kwargs2['persist_user_timestamp'] = getattr(s, 'pending_started_at', None)
if moa_config is not None:
_heal_kwargs2["moa_config"] = moa_config
_heal_result = _heal_agent.run_conversation(**_heal_kwargs2)
@@ -0,0 +1,58 @@
"""
Test for Issue #6935: Ensure agent.run_conversation() does not trip a TypeError
when persist_user_timestamp is passed to agent implementations that do not accept it.
"""
from api.streaming import _supports_kwarg
class LegacyAgentStub:
"""Agent stub mimicking older hermes-agent whose run_conversation does NOT accept **kwargs or persist_user_timestamp."""
def run_conversation(self, user_message, system_message=None, conversation_history=None, task_id=None, persist_user_message=None):
return {"final_response": "ok", "messages": []}
class ModernAgentWithKwargsStub:
"""Agent stub whose run_conversation accepts **kwargs."""
def run_conversation(self, **kwargs):
return {"final_response": "ok", "messages": []}
class ModernAgentWithExplicitParamStub:
"""Agent stub whose run_conversation explicitly accepts persist_user_timestamp."""
def run_conversation(self, user_message, persist_user_timestamp=None):
return {"final_response": "ok", "messages": []}
def test_supports_kwarg_detection():
legacy_agent = LegacyAgentStub()
modern_kwargs_agent = ModernAgentWithKwargsStub()
modern_explicit_agent = ModernAgentWithExplicitParamStub()
assert _supports_kwarg(legacy_agent.run_conversation, "persist_user_timestamp") is False
assert _supports_kwarg(modern_kwargs_agent.run_conversation, "persist_user_timestamp") is True
assert _supports_kwarg(modern_explicit_agent.run_conversation, "persist_user_timestamp") is True
def test_legacy_agent_run_conversation_kwargs_filtering():
legacy_agent = LegacyAgentStub()
kwargs = {
"user_message": "Hello",
"system_message": "System prompt",
"conversation_history": [],
"task_id": "test-session-123",
"persist_user_message": "Hello",
"persist_user_timestamp": 1700000000.0,
}
if not _supports_kwarg(legacy_agent.run_conversation, "persist_user_timestamp"):
kwargs.pop("persist_user_timestamp", None)
# Calling run_conversation with filtered kwargs must not raise TypeError
result = legacy_agent.run_conversation(**kwargs)
assert result["final_response"] == "ok"
if __name__ == "__main__":
test_supports_kwarg_detection()
test_legacy_agent_run_conversation_kwargs_filtering()
print("All tests passed successfully!")