Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] e5a1f7e683 fix: guard isatty() against ValueError on MCP stdio shutdown
When the MCP server runs over stdio transport, stdin/stdout are closed
before the call_on_close promo callback fires. sys.stdin.isatty() then
raises ValueError: I/O operation on closed file.

Wrap the isatty() calls in a try/except ValueError in
_is_interactive_session() so the promo is silently skipped rather than
crashing the shutdown path.

Fixes #607

Co-authored-by: bm-clawd <bm-clawd@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-25 17:56:38 +00:00
2 changed files with 45 additions and 1 deletions
+8 -1
View File
@@ -24,7 +24,14 @@ 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 objects; treat as non-interactive
# so the promo is suppressed rather than crashing the shutdown path.
# Outcome: promo skipped for this invocation.
return False
def _build_cloud_promo_message() -> str:
+37
View File
@@ -20,6 +20,43 @@ def _capture_console() -> tuple[Console, StringIO]:
return Console(file=buf, force_terminal=True), buf
# --- _is_interactive_session tests ---
def test_is_interactive_session_returns_false_on_closed_stdin(monkeypatch):
"""isatty() raises ValueError when stdio is closed (e.g., MCP shutdown)."""
import sys
from basic_memory.cli.promo import _is_interactive_session
class _ClosedStdin:
def isatty(self):
raise ValueError("I/O operation on closed file")
monkeypatch.setattr(sys, "stdin", _ClosedStdin())
assert _is_interactive_session() is False
def test_is_interactive_session_returns_false_on_closed_stdout(monkeypatch):
"""isatty() raises ValueError on closed stdout (e.g., MCP shutdown)."""
import sys
from basic_memory.cli.promo import _is_interactive_session
class _ClosedStdout:
def isatty(self):
raise ValueError("I/O operation on closed file")
# stdin reports interactive, but stdout is closed
class _InteractiveStdin:
def isatty(self):
return True
monkeypatch.setattr(sys, "stdin", _InteractiveStdin())
monkeypatch.setattr(sys, "stdout", _ClosedStdout())
assert _is_interactive_session() is False
# --- maybe_show_init_line tests ---