Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions hermes_cli/cli_process_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ def _owns_process_notification(self, event: dict) -> bool:
resolved_key = event_key
return str(resolved_key) == current_key

def _background_notifications_suppressed(self) -> bool:
"""Whether ``display.background_process_notifications`` is ``off`` for this CLI session.

The key gates the gateway's completion injection (#9290) but the CLI drain never consulted
it, so the documented ``off`` escape hatch silently did nothing here (#123114). Mirrors the
gateway semantics: events are still drained, claimed and acknowledged — only the
turn-starting injection is suppressed."""
try:
from cli import CLI_CONFIG
mode = str((CLI_CONFIG.get("display") or {}).get("background_process_notifications") or "").strip().lower()
except Exception:
return False
return mode == "off"

def _drain_process_notifications(self, consumer: str) -> None:
from tools.process_registry import process_registry
from tools.async_delegation import claim_event_delivery, complete_event_delivery
Expand All @@ -34,6 +48,8 @@ def _drain_process_notifications(self, consumer: str) -> None:
continue
claimed.append((event, text))
complete_event_delivery(event, claim)
if self._background_notifications_suppressed():
return
for notifications in group_process_notifications(claimed):
event, text = notifications[0]
if event.get("type", "completion") == "completion":
Expand Down
22 changes: 22 additions & 0 deletions tests/hermes_cli/test_process_notification_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from types import SimpleNamespace
from unittest.mock import Mock

import pytest

from cli import HermesCLI
from tools.process_registry_notifications import (
PROCESS_COMPLETE_DISPLAY_KIND, format_process_notification, process_completion_display_text)
Expand All @@ -18,6 +20,26 @@ def _event(sid, exit_code, command="cd /tmp && bash long-build.sh"):
return {"type": "completion", "session_id": sid, "session_key": "display-session", "command": command,
"exit_code": exit_code, "completion_reason": "exited", "output": "web tsc=0\nSECRET_OUTPUT_LINE"}

@pytest.mark.parametrize("mode,expect_injected", [("off", False), ("concise", True), ("all", True)])
def test_drain_honors_background_process_notifications_off(monkeypatch, mode, expect_injected):
"""``off`` consumes completion events without injecting a REPL input (no spurious post-Ctrl+C
turn, #123114); every other documented mode keeps injecting, and the durable claim/ack chain
runs in all modes so an ``off`` session never leaves pending rows to replay on restart."""
events = [_event("proc_off", 0)]
cli = HermesCLI.__new__(HermesCLI)
cli.session_id = "display-session"
cli._pending_input = queue.Queue()
monkeypatch.setattr("tools.process_registry.process_registry", _registry(events))
acked = []
monkeypatch.setattr("tools.async_delegation.claim_event_delivery", lambda *a: "claimed")
monkeypatch.setattr("tools.async_delegation.complete_event_delivery", lambda *a: acked.append(a))
monkeypatch.setattr("cli.CLI_CONFIG", {"display": {"background_process_notifications": mode}})
cli._drain_process_notifications("cli-post-turn")
assert acked, "events must still be claimed and acknowledged under every mode"
assert cli._pending_input.empty() is (not expect_injected)
if expect_injected:
assert cli._pending_input.get_nowait() is not None

def test_process_completion_display_keeps_payload_separate_across_surfaces(monkeypatch, capsys, tmp_path):
events = [_event("proc_1", 0)]
payload = format_process_notification(events[0])
Expand Down