diff --git a/src/basic_memory/cli/promo.py b/src/basic_memory/cli/promo.py index 7139500d..b5965fc8 100644 --- a/src/basic_memory/cli/promo.py +++ b/src/basic_memory/cli/promo.py @@ -24,7 +24,13 @@ def _promos_disabled_by_env() -> bool: def _is_interactive_session() -> bool: """Return whether stdin/stdout are interactive terminals.""" - return sys.stdin.isatty() and sys.stdout.isatty() + try: + return sys.stdin.isatty() and sys.stdout.isatty() + except ValueError: + # Trigger: stdin/stdout already closed (e.g., MCP stdio transport shutdown) + # Why: isatty() raises ValueError on closed file descriptors + # Outcome: treat as non-interactive, suppressing promo output + return False def _build_cloud_promo_message() -> str: diff --git a/src/basic_memory/services/project_service.py b/src/basic_memory/services/project_service.py index a5358405..97244e0f 100644 --- a/src/basic_memory/services/project_service.py +++ b/src/basic_memory/services/project_service.py @@ -942,19 +942,21 @@ class ProjectService: is_postgres = config.database_backend == DatabaseBackend.POSTGRES # --- Check vector table existence --- + # Both search_vector_chunks and search_vector_embeddings must exist + # for the detailed stats queries (JOINs between them) to work. if is_postgres: table_check_sql = text( "SELECT COUNT(*) FROM information_schema.tables " - "WHERE table_name = 'search_vector_chunks'" + "WHERE table_name IN ('search_vector_chunks', 'search_vector_embeddings')" ) else: table_check_sql = text( "SELECT COUNT(*) FROM sqlite_master " - "WHERE type = 'table' AND name = 'search_vector_chunks'" + "WHERE type = 'table' AND name IN ('search_vector_chunks', 'search_vector_embeddings')" ) table_result = await self.repository.execute_query(table_check_sql, {}) - vector_tables_exist = (table_result.scalar() or 0) > 0 + vector_tables_exist = (table_result.scalar() or 0) == 2 if not vector_tables_exist: # Count distinct entities in search index for the recommendation message diff --git a/tests/cli/test_cloud_promo.py b/tests/cli/test_cloud_promo.py index 5451dc19..823703d7 100644 --- a/tests/cli/test_cloud_promo.py +++ b/tests/cli/test_cloud_promo.py @@ -8,6 +8,7 @@ from typer.testing import CliRunner from basic_memory.cli.app import app import basic_memory from basic_memory.cli.promo import ( + _is_interactive_session, maybe_show_cloud_promo, maybe_show_init_line, ) @@ -294,3 +295,17 @@ def test_cloud_promo_command_on_clears_opt_out(monkeypatch): assert "Cloud promo messages enabled" in result.stdout assert len(instances) == 1 assert instances[0].saved_config.cloud_promo_opt_out is False + + +# --- _is_interactive_session tests --- + + +def test_is_interactive_session_returns_false_when_streams_closed(monkeypatch): + """isatty() raises ValueError on closed file descriptors (e.g., MCP shutdown).""" + + class ClosedStream: + def isatty(self): + raise ValueError("I/O operation on closed file") + + monkeypatch.setattr("sys.stdin", ClosedStream()) + assert _is_interactive_session() is False