mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-20 22:51:07 +00:00
Merge pull request #4426 from nesquena/release/stage-4417
Release v0.51.493 — RC (ignore malformed background-process wakeup events, #4417)
This commit is contained in:
@@ -3,6 +3,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.493] — 2026-06-18 — Release RC (ignore malformed background-process wakeup events)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Malformed background-process wakeup events no longer appear as empty `unknown completed` prompts.** WebUI now ignores empty or unknown completion-queue payloads and renders watch-pattern overflow summaries as explicit watch notifications instead of fake process completions. Thanks @ai-ag2026.
|
||||
|
||||
## [v0.51.492] — 2026-06-18 — Release RB (large context window preserved on session reload)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -350,17 +350,28 @@ def _truncate(text: str, limit: int) -> str:
|
||||
return s[:limit] + "\n…(truncated)"
|
||||
|
||||
|
||||
def format_wakeup_prompt(evt: dict) -> str:
|
||||
def format_wakeup_prompt(evt: object) -> str | None:
|
||||
"""Build the synthetic [IMPORTANT: …] message the agent will see.
|
||||
|
||||
Mirrors ``cli._format_process_notification`` so wakeup payloads look the
|
||||
same in CLI and WebUI sessions.
|
||||
"""
|
||||
if not isinstance(evt, dict) or not evt:
|
||||
return None
|
||||
|
||||
evt_type = evt.get("type", "completion")
|
||||
sid = evt.get("session_id", "unknown")
|
||||
cmd = evt.get("command", "unknown")
|
||||
sid = str(evt.get("session_id") or "").strip()
|
||||
cmd = str(evt.get("command") or "").strip()
|
||||
# The current server-side wakeup drain drops global watch-overflow events
|
||||
# before this formatter because they intentionally carry no session_key.
|
||||
# Keep this branch defensive so any future routable overflow summary is not
|
||||
# mis-rendered as a fake process completion.
|
||||
if evt_type in {"watch_overflow_tripped", "watch_overflow_released"}:
|
||||
msg = str(evt.get("message") or "").strip()
|
||||
return f"[IMPORTANT: {msg}]" if msg else None
|
||||
if evt_type == "watch_disabled":
|
||||
return f"[IMPORTANT: {evt.get('message', '')}]"
|
||||
msg = str(evt.get("message") or "").strip()
|
||||
return f"[IMPORTANT: {msg}]" if msg else None
|
||||
if evt_type == "watch_match":
|
||||
pat = evt.get("pattern", "?")
|
||||
out = _truncate(evt.get("output", ""), 4000)
|
||||
@@ -373,6 +384,12 @@ def format_wakeup_prompt(evt: dict) -> str:
|
||||
if sup:
|
||||
body += f"\n({sup} earlier matches were suppressed by rate limit)"
|
||||
return body + "]"
|
||||
if evt_type != "completion":
|
||||
return None
|
||||
|
||||
if not (sid or cmd or "exit_code" in evt or evt.get("output")):
|
||||
return None
|
||||
|
||||
# Default: completion event
|
||||
exit_code = evt.get("exit_code", "?")
|
||||
out = _truncate(evt.get("output", ""), 4000)
|
||||
@@ -880,7 +897,8 @@ def _process_one(evt: dict) -> None:
|
||||
# `{session_id, task_id, completed_at, summary?, event_id}`, so
|
||||
# we derive the prompt directly from the evt here (same source the
|
||||
# prior _build_payload used).
|
||||
wakeup_prompt = format_wakeup_prompt(evt).strip()
|
||||
wakeup_prompt_raw = format_wakeup_prompt(evt)
|
||||
wakeup_prompt = wakeup_prompt_raw.strip() if wakeup_prompt_raw else ""
|
||||
if wakeup_prompt:
|
||||
if _session_has_active_turn(session_id):
|
||||
# Defer-path fix: persist the prompt so a turn-teardown
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
from api.background_process import format_wakeup_prompt
|
||||
|
||||
|
||||
def test_format_wakeup_prompt_skips_empty_event():
|
||||
assert format_wakeup_prompt({}) is None
|
||||
|
||||
|
||||
def test_format_wakeup_prompt_skips_non_dict_event():
|
||||
assert format_wakeup_prompt(None) is None
|
||||
assert format_wakeup_prompt(42) is None
|
||||
|
||||
|
||||
def test_format_wakeup_prompt_skips_unknown_event_type():
|
||||
evt = {"type": "unknown_event", "message": "do not inject me"}
|
||||
assert format_wakeup_prompt(evt) is None
|
||||
|
||||
|
||||
def test_format_wakeup_prompt_handles_watch_disabled():
|
||||
evt = {
|
||||
"type": "watch_disabled",
|
||||
"session_id": "proc_abc",
|
||||
"session_key": "webui-session",
|
||||
"command": "tail -f log",
|
||||
"message": "Watch patterns disabled for process proc_abc.",
|
||||
}
|
||||
assert (
|
||||
format_wakeup_prompt(evt)
|
||||
== "[IMPORTANT: Watch patterns disabled for process proc_abc.]"
|
||||
)
|
||||
|
||||
|
||||
def test_format_wakeup_prompt_skips_blank_watch_disabled():
|
||||
assert format_wakeup_prompt({"type": "watch_disabled"}) is None
|
||||
assert format_wakeup_prompt({"type": "watch_disabled", "message": ""}) is None
|
||||
|
||||
|
||||
def test_format_wakeup_prompt_handles_watch_overflow_tripped():
|
||||
evt = {
|
||||
"type": "watch_overflow_tripped",
|
||||
"session_id": "",
|
||||
"session_key": "",
|
||||
"command": "",
|
||||
"message": "Watch-pattern overflow: suppressing further watch_match events.",
|
||||
}
|
||||
assert (
|
||||
format_wakeup_prompt(evt)
|
||||
== "[IMPORTANT: Watch-pattern overflow: suppressing further watch_match events.]"
|
||||
)
|
||||
|
||||
|
||||
def test_format_wakeup_prompt_handles_watch_overflow_released():
|
||||
evt = {
|
||||
"type": "watch_overflow_released",
|
||||
"session_id": "",
|
||||
"session_key": "",
|
||||
"command": "",
|
||||
"suppressed": 3,
|
||||
"message": "Watch-pattern notifications resumed. 3 match event(s) were suppressed.",
|
||||
}
|
||||
assert (
|
||||
format_wakeup_prompt(evt)
|
||||
== "[IMPORTANT: Watch-pattern notifications resumed. 3 match event(s) were suppressed.]"
|
||||
)
|
||||
|
||||
|
||||
def test_format_wakeup_prompt_keeps_normal_completion():
|
||||
evt = {
|
||||
"type": "completion",
|
||||
"session_id": "proc_abc",
|
||||
"command": "sleep 1",
|
||||
"exit_code": 0,
|
||||
"output": "done",
|
||||
}
|
||||
result = format_wakeup_prompt(evt)
|
||||
assert result is not None
|
||||
assert "Background process proc_abc completed" in result
|
||||
assert "Command: sleep 1" in result
|
||||
assert "Output:\ndone" in result
|
||||
Reference in New Issue
Block a user