From ff0ac7c7a46847b2422f601a8d13cb92154c7f2a Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:17:27 +0200 Subject: [PATCH 1/3] fix: ignore malformed process wakeup events --- CHANGELOG.md | 4 ++ api/background_process.py | 24 +++++++-- .../test_background_process_wakeup_format.py | 54 +++++++++++++++++++ 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 tests/test_background_process_wakeup_format.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cf104f46..ef4f7c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### 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. + ## [v0.51.489] — 2026-06-18 — Release QY (outline button no longer collides with the scroll control) ### Fixed diff --git a/api/background_process.py b/api/background_process.py index 46801949..946598c3 100644 --- a/api/background_process.py +++ b/api/background_process.py @@ -350,17 +350,24 @@ def _truncate(text: str, limit: int) -> str: return s[:limit] + "\n…(truncated)" -def format_wakeup_prompt(evt: dict) -> str: +def format_wakeup_prompt(evt: dict) -> 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() + 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 +380,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 +893,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 diff --git a/tests/test_background_process_wakeup_format.py b/tests/test_background_process_wakeup_format.py new file mode 100644 index 00000000..cb8368a1 --- /dev/null +++ b/tests/test_background_process_wakeup_format.py @@ -0,0 +1,54 @@ +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_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_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 From 352e1893cb5ce7c8019844d70aa36cbecbfbb79e Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:30:24 +0200 Subject: [PATCH 2/3] test: cover malformed wakeup edge cases --- api/background_process.py | 6 ++++- .../test_background_process_wakeup_format.py | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/api/background_process.py b/api/background_process.py index 946598c3..ab82083f 100644 --- a/api/background_process.py +++ b/api/background_process.py @@ -350,7 +350,7 @@ def _truncate(text: str, limit: int) -> str: return s[:limit] + "\n…(truncated)" -def format_wakeup_prompt(evt: dict) -> str | None: +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 @@ -362,6 +362,10 @@ def format_wakeup_prompt(evt: dict) -> str | None: evt_type = evt.get("type", "completion") 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 diff --git a/tests/test_background_process_wakeup_format.py b/tests/test_background_process_wakeup_format.py index cb8368a1..16e2bcf1 100644 --- a/tests/test_background_process_wakeup_format.py +++ b/tests/test_background_process_wakeup_format.py @@ -5,11 +5,35 @@ 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", From 18d5df74a929c058384a7bb44a9a43292f3c6bcb Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 18 Jun 2026 18:04:28 +0000 Subject: [PATCH 3/3] =?UTF-8?q?release:=20v0.51.493=20=E2=80=94=20ignore?= =?UTF-8?q?=20malformed=20background-process=20wakeup=20events=20(#4417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69df2f53..7dfcc1b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## [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. +- **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)