Skip to content
Open
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
152 changes: 152 additions & 0 deletions .pr/diagnose_echo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Diagnose terminal echo of escape sequence responses.

The problem: When terminal queries are sent, the terminal responds by:
1. Writing the response to stdin (the "leak" we're flushing)
2. ECHOING the response to stdout (the visible noise)

This script tests whether we can suppress the echo.
"""

import os
import select
import sys
import termios
import time


def send_query_normal_mode():
"""Send query in normal terminal mode - expect visible echo."""
print("\n[Normal mode] Sending DSR query...")
sys.stdout.write("\x1b[6n")
sys.stdout.flush()
time.sleep(0.2)


def send_query_no_echo_mode():
"""Send query with echo disabled - should suppress visible response."""
print("\n[No-echo mode] Sending DSR query...")

# Save current terminal settings
old_settings = termios.tcgetattr(sys.stdin)

try:
# Disable echo (ECHO flag)
new_settings = list(old_settings)
new_settings[3] &= ~termios.ECHO # Disable echo
termios.tcsetattr(sys.stdin, termios.TCSANOW, new_settings)

# Send the query
sys.stdout.write("\x1b[6n")
sys.stdout.flush()

# Wait for response
time.sleep(0.2)

# Read and discard the response from stdin
new_settings[3] &= ~termios.ICANON # Also disable canonical mode for reading
new_settings[6][termios.VMIN] = 0
new_settings[6][termios.VTIME] = 0
termios.tcsetattr(sys.stdin, termios.TCSANOW, new_settings)

if select.select([sys.stdin], [], [], 0)[0]:
data = os.read(sys.stdin.fileno(), 4096)
print(f" (Flushed {len(data)} bytes from stdin: {data!r})")

finally:
# Restore original settings
termios.tcsetattr(sys.stdin, termios.TCSANOW, old_settings)

print(" Query sent with echo disabled")


def send_query_raw_mode():
"""Send query in raw mode - full control."""
print("\n[Raw mode] Sending DSR query...")

old_settings = termios.tcgetattr(sys.stdin)

try:
# Enter raw mode (no echo, no canonical, no signals)
new_settings = list(old_settings)
new_settings[3] &= ~(termios.ECHO | termios.ICANON | termios.ISIG)
new_settings[6][termios.VMIN] = 0
new_settings[6][termios.VTIME] = 0
termios.tcsetattr(sys.stdin, termios.TCSANOW, new_settings)

# Send the query
sys.stdout.write("\x1b[6n")
sys.stdout.flush()

# Wait and read response
time.sleep(0.2)

if select.select([sys.stdin], [], [], 0)[0]:
data = os.read(sys.stdin.fileno(), 4096)
print(f" (Flushed {len(data)} bytes from stdin: {data!r})")

finally:
termios.tcsetattr(sys.stdin, termios.TCSANOW, old_settings)

print(" Query sent in raw mode")


def main():
print("=" * 60)
print("TERMINAL ECHO DIAGNOSTIC")
print("=" * 60)
print(f"stdin.isatty(): {sys.stdin.isatty()}")
print(f"stdout.isatty(): {sys.stdout.isatty()}")

if not sys.stdout.isatty():
print("\n⚠️ Run directly in terminal, not piped!")
return

# Test 1: Normal mode (expect visible echo)
send_query_normal_mode()
print(" ^ Did you see escape codes above this line?")

# Test 2: Echo disabled
send_query_no_echo_mode()
print(" ^ Should be NO visible escape codes")

# Test 3: Raw mode
send_query_raw_mode()
print(" ^ Should be NO visible escape codes")

# Test 4: Multiple queries with echo suppression
print("\n[Multiple queries with echo suppressed]")
old_settings = termios.tcgetattr(sys.stdin)
try:
new_settings = list(old_settings)
new_settings[3] &= ~(termios.ECHO | termios.ICANON)
new_settings[6][termios.VMIN] = 0
new_settings[6][termios.VTIME] = 0
termios.tcsetattr(sys.stdin, termios.TCSANOW, new_settings)

for i in range(3):
sys.stdout.write("\x1b[6n") # DSR
sys.stdout.write("\x1b]11;?\x07") # OSC 11
sys.stdout.flush()
time.sleep(0.3)

total = 0
while select.select([sys.stdin], [], [], 0)[0]:
data = os.read(sys.stdin.fileno(), 4096)
if not data:
break
total += len(data)
print(f" Flushed {total} bytes, no visible echo")

finally:
termios.tcsetattr(sys.stdin, termios.TCSANOW, old_settings)

print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print("If echo suppression works, we can modify flush_stdin() to")
print("disable echo BEFORE flushing, preventing visible noise.")


if __name__ == "__main__":
main()
105 changes: 105 additions & 0 deletions .pr/diagnose_leak.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Diagnose stdin escape code leak.

Run this DIRECTLY in a terminal (not piped):
uv run python .pr/diagnose_leak.py

Watch for garbage appearing AFTER the script exits.
"""

import os
import select
import sys
import time


def check_stdin_raw() -> bytes:
"""Read pending stdin data without blocking."""
if not sys.stdin.isatty():
return b""
try:
import termios

old = termios.tcgetattr(sys.stdin)
new = list(old)
new[3] &= ~(termios.ICANON | termios.ECHO)
new[6][termios.VMIN] = 0
new[6][termios.VTIME] = 0
termios.tcsetattr(sys.stdin, termios.TCSANOW, new)
data = b""
if select.select([sys.stdin], [], [], 0)[0]:
data = os.read(sys.stdin.fileno(), 4096)
termios.tcsetattr(sys.stdin, termios.TCSANOW, old)
return data
except Exception as e:
print(f"Error: {e}")
return b""


def main():
from openhands.sdk.logger import flush_stdin, logger as logger_module

print("=" * 60)
print("STDIN LEAK DIAGNOSTIC")
print("=" * 60)
print(f"stdin.isatty(): {sys.stdin.isatty()}")
print(f"stdout.isatty(): {sys.stdout.isatty()}")

if not sys.stdout.isatty():
print("\n⚠️ WARNING: stdout is not a TTY!")
print(" Terminal queries won't get responses.")
print(" Run directly in terminal, not piped.")

# Test 1: Send query, wait, check stdin
print("\n[Test 1] Send DSR query, check stdin after 200ms")
sys.stdout.write("\x1b[6n")
sys.stdout.flush()
time.sleep(0.2)
data1 = check_stdin_raw()
print(f" stdin data: {data1!r}")

# Test 2: Send query, flush with SDK, check what was caught
print("\n[Test 2] Send DSR query, call flush_stdin()")
sys.stdout.write("\x1b[6n")
sys.stdout.flush()
time.sleep(0.2)
flushed = flush_stdin()
preserved = logger_module._preserved_input_buffer
print(f" Bytes flushed: {flushed}")
print(f" Preserved buffer: {preserved!r}")

# Test 3: OSC 11 query
print("\n[Test 3] Send OSC 11 query (background color)")
sys.stdout.write("\x1b]11;?\x07")
sys.stdout.flush()
time.sleep(0.2)
flushed2 = flush_stdin()
preserved2 = logger_module._preserved_input_buffer
print(f" Bytes flushed: {flushed2}")
print(f" Preserved buffer: {preserved2!r}")

# Test 4: Multiple rapid queries (simulates Rich/gh behavior)
print("\n[Test 4] Multiple rapid queries then flush")
for _ in range(3):
sys.stdout.write("\x1b[6n")
sys.stdout.write("\x1b]11;?\x07")
sys.stdout.flush()
time.sleep(0.3)
flushed3 = flush_stdin()
preserved3 = logger_module._preserved_input_buffer
print(f" Bytes flushed: {flushed3}")
print(f" Preserved buffer: {preserved3!r}")

# Final flush before exit
print("\n[Final] Calling flush_stdin() before exit")
final_flushed = flush_stdin()
print(f" Final bytes flushed: {final_flushed}")

print("\n" + "=" * 60)
print("DONE - Watch for garbage on the next shell prompt!")
print("If you see '^[[...R' or 'rgb:...' after '$', the leak persists.")
print("=" * 60)


if __name__ == "__main__":
main()
104 changes: 104 additions & 0 deletions .pr/diagnose_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Diagnose: Are escape codes in PTY output or terminal responses?

This script runs the terminal tool directly and captures its raw output
WITHOUT displaying it to the terminal. This lets us see if escape codes
are being captured as part of the command output vs generated by terminal
responses.
"""

import re
import sys

from openhands.tools.terminal.definition import TerminalAction
from openhands.tools.terminal.terminal.factory import create_terminal_session


print("=" * 60)
print("DIAGNOSTIC: Source of Escape Codes")
print("=" * 60)
print(f"stdin.isatty(): {sys.stdin.isatty()}")
print(f"stdout.isatty(): {sys.stdout.isatty()}")
print()


print("Creating terminal session...")
session = create_terminal_session(
work_dir="/tmp",
username=None,
no_change_timeout_seconds=30,
)
session.initialize()

print("Running: gh pr list --repo OpenHands/openhands --limit 3")
print()

# Execute command and capture output
action = TerminalAction(
command="gh pr list --repo OpenHands/openhands --limit 3",
is_input=False,
timeout=30,
)
result = session.execute(action)

print("=" * 60)
print("RAW CAPTURED OUTPUT (repr):")
print("=" * 60)
# Show the raw output with escape codes visible
raw_output = result.text if hasattr(result, "text") else str(result)
print(repr(raw_output))

print()
print("=" * 60)
print("ESCAPE SEQUENCE ANALYSIS:")
print("=" * 60)

# Check for common escape sequences
escape_patterns = [
(b"\x1b[", "CSI (Control Sequence Introducer)"),
(b"\x1b]", "OSC (Operating System Command)"),
(b"\x1bP", "DCS (Device Control String)"),
(b"\x1b\\", "ST (String Terminator)"),
]

raw_bytes = raw_output.encode("utf-8", errors="replace")
found_any = False

for pattern, name in escape_patterns:
count = raw_bytes.count(pattern)
if count > 0:
found_any = True
print(f" Found {count}x {name} sequences")

# Find and show the sequences
if pattern == b"\x1b[":
# CSI sequences end with a letter
matches = re.findall(rb"\x1b\[[0-9;]*[A-Za-z]", raw_bytes)
for m in matches[:5]: # Show up to 5
print(f" - {m!r}")
elif pattern == b"\x1b]":
# OSC sequences end with BEL or ST
matches = re.findall(rb"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", raw_bytes)
for m in matches[:5]:
print(f" - {m!r}")

if not found_any:
print(" No escape sequences found in captured output!")

print()
print("=" * 60)
print("CONCLUSION:")
print("=" * 60)

if found_any:
print(" Escape codes ARE in the PTY captured output.")
print(" The gh command is writing these to its stdout.")
print(" FIX: Filter escape sequences from terminal tool output.")
else:
print(" Escape codes are NOT in captured output.")
print(" They must be coming from terminal responses to Rich queries.")
print(" FIX: Echo suppression should work (check implementation).")

print()
session.close()
print("Session closed.")
Loading
Loading