diff --git a/raven/cli/_memory_warn.py b/raven/cli/_memory_warn.py new file mode 100644 index 0000000..fa4cbd2 --- /dev/null +++ b/raven/cli/_memory_warn.py @@ -0,0 +1,21 @@ +"""User-visible warning when the memory backend fails to start. + +Under the TUI, loguru is redirected to a file, so a start failure (e.g. a +memory identity that EverOS rejects) silently disables long-term memory with +nothing on screen. Mirror the embedding-unavailable warning in +``everos/backend.py`` and print to stderr so every entry point (TUI, agent +REPL, gateway) sees the same degraded-memory notice, not just the two that +already surface the traceback via stderr. +""" + +from __future__ import annotations + +from rich.console import Console + + +def warn_memory_start_failed(exc: BaseException) -> None: + """Print a stderr notice that long-term memory is off this session.""" + Console(stderr=True).print( + "[yellow]Memory backend failed to start; long-term memory is off this session.[/yellow]\n" + f"[dim]Run `raven onboard` to reconfigure, or check the log ({type(exc).__name__}).[/dim]" + ) diff --git a/raven/cli/agent_commands.py b/raven/cli/agent_commands.py index 715c64f..6e34300 100644 --- a/raven/cli/agent_commands.py +++ b/raven/cli/agent_commands.py @@ -398,10 +398,13 @@ async def run_once(): if backend is not None: try: await backend.start() - except Exception: + except Exception as exc: logger.exception( "memory backend start failed; continuing with legacy memory path", ) + from raven.cli._memory_warn import warn_memory_start_failed + + warn_memory_start_failed(exc) try: # Build inside the running loop: Scheduler pins its home loop in # __init__, so build_repl must not run in the sync prologue. @@ -496,10 +499,13 @@ async def run_interactive(): if backend is not None: try: await backend.start() - except Exception: + except Exception as exc: logger.exception( "memory backend start failed; continuing with legacy memory path", ) + from raven.cli._memory_warn import warn_memory_start_failed + + warn_memory_start_failed(exc) # agent_loop.run() is now a lifecycle keep-alive (executor / # debug server / MCP up, then idle); all turns go through the # spine. Gathered on teardown. diff --git a/raven/cli/gateway_commands.py b/raven/cli/gateway_commands.py index 9edfccc..ce79351 100644 --- a/raven/cli/gateway_commands.py +++ b/raven/cli/gateway_commands.py @@ -368,10 +368,13 @@ async def run(): if backend is not None: try: await backend.start() - except Exception: + except Exception as exc: _logger.exception( "memory backend start failed; continuing with legacy memory path", ) + from raven.cli._memory_warn import warn_memory_start_failed + + warn_memory_start_failed(exc) try: # Spine assembly for the gateway's host sources (cron submits # through it, replies route to channels via a per-channel outlet). diff --git a/raven/cli/tui_commands.py b/raven/cli/tui_commands.py index 3c53367..5cf994d 100644 --- a/raven/cli/tui_commands.py +++ b/raven/cli/tui_commands.py @@ -706,12 +706,15 @@ def _agent_loop_factory(): async def _start_backend() -> None: try: await agent_loop.backend.start() # type: ignore[union-attr] - except Exception: + except Exception as exc: from loguru import logger as _logger _logger.exception( "tui: memory backend start failed; continuing with degraded memory path", ) + from raven.cli._memory_warn import warn_memory_start_failed + + warn_memory_start_failed(exc) _strip_tty_stream_handlers() asyncio.create_task(_start_backend()) diff --git a/tests/test_memory_warn.py b/tests/test_memory_warn.py new file mode 100644 index 0000000..0d1da6d --- /dev/null +++ b/tests/test_memory_warn.py @@ -0,0 +1,19 @@ +"""warn_memory_start_failed must surface on stderr (visible under the TUI).""" + +from __future__ import annotations + +import pytest + +from raven.cli._memory_warn import warn_memory_start_failed + + +def test_warn_memory_start_failed_writes_degraded_notice_to_stderr( + capsys: pytest.CaptureFixture[str], +) -> None: + warn_memory_start_failed(ValueError("memory.userId='..' is not accepted")) + + captured = capsys.readouterr() + combined = captured.err + captured.out + + assert "long-term memory is off" in combined + assert "ValueError" in combined