AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

ShellSession Times Out When Command Output Lacks Trailing Newline

ShellSession._collect_output uses readline() and checks data.startswith(marker), so when the last output line lacks a trailing newline, the marker is merged with the output, the check fails, and the command times out.

highConfidence 95%LangChainAffected V<=1.2.10

Origin Analysis

The _collect_output method reads stdout with readline(), which buffers until a newline. If the command's final output line has no trailing newline, the next write (the done marker) is appended to the same line. The resulting string starts with the command output, not the marker, so data.startswith(marker) is false, and the loop blocks until the full command_timeout expires.
Run the following Python script with langchain==1.2.10 (or earlier): ```python import time from pathlib import Path from langchain.agents.middleware._execution import HostExecutionPolicy from langchain.agents.middleware.shell_tool import ShellSession WORKSPACE = Path(__file__).parent / "scratch" / "reprod" WORKSPACE.mkdir(parents=True, exist_ok=True) WITH_NEWLINE = WORKSPACE / "with_newline.json" NO_NEWLINE = WORKSPACE / "no_newline.json" WITH_NEWLINE.write_text('{"k":"v"}\n') # normal file NO_NEWLINE.write_bytes(b'{"k":"v"}') # no trailing newline — triggers the bug policy = HostExecutionPolicy(command_timeout=5) session = ShellSession( workspace=WORKSPACE, policy=policy, command=("/bin/bash",), environment={}, ) session.start() for label, path in [("with newline ", WITH_NEWLINE), ("without newline", NO_NEWLINE)]: t0 = time.monotonic() result = session.execute(f"head -n 1 {path}", timeout=policy.command_timeout) elapsed = time.monotonic() - t0 status = "TIMEOUT" if result.timed_out else "OK" print(f"[{status}] {label} output={result.output!r} exit={result.exit_code} {elapsed:.2f}s") session.stop(policy.termination_timeout) ``` Expected: both commands complete immediately; actual: the second command times out after 5 seconds with output='' and exit=None.

Fixing Code Block

Edge Case Audit

This fix assumes the marker string does not appear in legitimate command output; although the marker includes a UUID and is unique per session, a command could theoretically echo the exact marker. To mitigate, ensure the marker is generated with sufficient randomness and consider using a delimiter that includes a newline (e.g., marker = f'\n__LC_SHELL_DONE__{uuid} ') so readline always separates output from marker. If the marker can appear in output, a false positive would truncate output and misparse the exit code. Rollback: revert to the original startswith check, but that will reintroduce the timeout bug for no-newline outputs. No concurrency implications beyond the existing single-threaded ShellSession usage.

Ecosystem Topology