Skip to content

Commit e46555b

Browse files
phernandezclaude
andcommitted
fix: guard against closed streams in promo and missing vector tables (#579, #607)
- Wrap isatty() in _is_interactive_session() with try/except ValueError so MCP stdio transport shutdown no longer produces noisy tracebacks - Check both search_vector_chunks AND search_vector_embeddings exist before running JOIN queries in get_embedding_status(), fixing OperationalError when only the chunks table is present - Add test for closed-stream scenario Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 74e6afd commit e46555b

3 files changed

Lines changed: 27 additions & 4 deletions

File tree

src/basic_memory/cli/promo.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ def _promos_disabled_by_env() -> bool:
2424

2525
def _is_interactive_session() -> bool:
2626
"""Return whether stdin/stdout are interactive terminals."""
27-
return sys.stdin.isatty() and sys.stdout.isatty()
27+
try:
28+
return sys.stdin.isatty() and sys.stdout.isatty()
29+
except ValueError:
30+
# Trigger: stdin/stdout already closed (e.g., MCP stdio transport shutdown)
31+
# Why: isatty() raises ValueError on closed file descriptors
32+
# Outcome: treat as non-interactive, suppressing promo output
33+
return False
2834

2935

3036
def _build_cloud_promo_message() -> str:

src/basic_memory/services/project_service.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -942,19 +942,21 @@ async def get_embedding_status(self, project_id: int) -> EmbeddingStatus:
942942
is_postgres = config.database_backend == DatabaseBackend.POSTGRES
943943

944944
# --- Check vector table existence ---
945+
# Both search_vector_chunks and search_vector_embeddings must exist
946+
# for the detailed stats queries (JOINs between them) to work.
945947
if is_postgres:
946948
table_check_sql = text(
947949
"SELECT COUNT(*) FROM information_schema.tables "
948-
"WHERE table_name = 'search_vector_chunks'"
950+
"WHERE table_name IN ('search_vector_chunks', 'search_vector_embeddings')"
949951
)
950952
else:
951953
table_check_sql = text(
952954
"SELECT COUNT(*) FROM sqlite_master "
953-
"WHERE type = 'table' AND name = 'search_vector_chunks'"
955+
"WHERE type = 'table' AND name IN ('search_vector_chunks', 'search_vector_embeddings')"
954956
)
955957

956958
table_result = await self.repository.execute_query(table_check_sql, {})
957-
vector_tables_exist = (table_result.scalar() or 0) > 0
959+
vector_tables_exist = (table_result.scalar() or 0) == 2
958960

959961
if not vector_tables_exist:
960962
# Count distinct entities in search index for the recommendation message

tests/cli/test_cloud_promo.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from basic_memory.cli.app import app
99
import basic_memory
1010
from basic_memory.cli.promo import (
11+
_is_interactive_session,
1112
maybe_show_cloud_promo,
1213
maybe_show_init_line,
1314
)
@@ -294,3 +295,17 @@ def save_config(self, config):
294295
assert "Cloud promo messages enabled" in result.stdout
295296
assert len(instances) == 1
296297
assert instances[0].saved_config.cloud_promo_opt_out is False
298+
299+
300+
# --- _is_interactive_session tests ---
301+
302+
303+
def test_is_interactive_session_returns_false_when_streams_closed(monkeypatch):
304+
"""isatty() raises ValueError on closed file descriptors (e.g., MCP shutdown)."""
305+
306+
class ClosedStream:
307+
def isatty(self):
308+
raise ValueError("I/O operation on closed file")
309+
310+
monkeypatch.setattr("sys.stdin", ClosedStream())
311+
assert _is_interactive_session() is False

0 commit comments

Comments
 (0)