Skip to content

Fix/pty command execution - #175

Open
kartikloops wants to merge 11 commits into
The-OpenROAD-Project:mainfrom
kartikloops:fix/pty-command-execution
Open

Fix/pty command execution#175
kartikloops wants to merge 11 commits into
The-OpenROAD-Project:mainfrom
kartikloops:fix/pty-command-execution

Conversation

@kartikloops

@kartikloops kartikloops commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

This branch fixes a set of real reliability bugs surfaced by dogfooding OpenROAD sessions: PTY command execution was broken by a fixed 80-column terminal that caused readline to redraw/scroll instead of returning clean output, and by terminal env defaults silently overriding caller-supplied TERM/COLUMNS/LINES (including a residual bug where an explicit COLUMNS=0 override was discarded); command completion detection was rewritten from a silence-window heuristic to an explicit runtime-generated sentinel marker, closing a false-positive where the PTY's own echo of the probe command could satisfy the startup handshake and a slow-but-healthy start could get misreported as unresponsive; createSession no longer holds the manager's global lock through the multi-second spawn-and-verify handshake, using a reserve-then-publish pattern so one stuck session can't stall every other session-manager call; report-image handling now accepts the doubled .webp.png extension some ORFS builds emit, reports the true image format instead of hardcoding webp, and anchors its "unrecognized file" hint regex to real extensions; and the server now auto-detects PATH/ORFS_FLOW_PATH at startup (merging login-shell PATH and common install locations when openroad isn't already reachable) so GUI-launched MCP clients no longer require manual .env setup, with docs updated to match.

OpenROAD runs a readline-style line editor when driven through a PTY. At the
hardcoded 80 columns that editor horizontally scrolls any command longer than
the window, redrawing the whole visible line once per character: a single
75-character read_liberty came back as a cascade of near-identical lines that
buried the real output.

Widen the terminal well past any realistic command and ask for a plain TERM so
the editor skips the cursor-addressing layer we cannot interpret anyway.

The terminal variables were also applied after the caller's env, silently
overriding it, so create_interactive_session's env parameter could never set
TERM, COLUMNS or LINES. Apply the defaults first and let the caller win.

Also adds PTY_LINE_TERMINATOR, used by the session layer in the next commit.
…e window

readOutput decided a command had finished once output went quiet for 100ms.
The PTY echoes the command back instantly, so that window closed while
OpenROAD was still working: read_db on a 15MB checkpoint and a 56s
repair_timing both returned the bare echo with error:null, indistinguishable
from success. timeout_ms was inert too, because the inner break abandoned the
whole loop on the first quiet moment rather than running to the deadline.

Write a nonce marker after each command and read until it appears. The Tcl
assembles the marker at runtime via join, so the literal never occurs in the
echoed input line and a match can only come from real stdout.

Consequences: timeout_ms now bounds the wait and reports CommandTimeout
instead of empty success, a process that dies mid-command reports
SessionTerminated, and execution_time is the command's real duration rather
than a flat 0.101s for everything.

Commands are also terminated with CR instead of LF. A line editor in raw mode
binds CR to accept-line; sending LF can leave the command sitting unsubmitted
in the edit buffer, which is how ten commands piled up without one executing.

readOutput is kept for buffer sampling and documented as unable to detect
completion, with a characterisation test pinning that behaviour so nobody
routes commands back through it. Output accumulation is capped at the buffer
size, keeping the tail, so a verbose repair_timing cannot grow the heap.
An OpenROAD process can come up looking perfectly healthy -- alive, prompt
printed, responding to inspect -- while its line editor never accepts the
lines we write. Every command then silently does nothing. In one 23-minute
run both sessions reported ten successful commands with 00:00:00 of CPU and
RSS pinned at the startup footprint; nothing had been loaded or computed.

createSession now runs a probe command through the sentinel path and fails if
it does not come back, so this turns into an obvious error within seconds of
session creation instead of a run's worth of results that were never real.
The probe is stripped from the audit trail afterwards.

The failure path also terminates the session. start() may already have spawned
a process, and previously a rejected creation dropped it from the registry
while leaving the OpenROAD process orphaned.
list_report_images returned total_images:0 for a directory holding five real
images. save_images does not name them consistently: this build wrote
final_all.webp.png and final_routing.webp.png, PNGs carrying a doubled
extension, and the filter only matched a trailing ".webp".

Accept .webp.png, .webp and .png, and strip the doubled form before
classification so final_all.webp.png still resolves to complete_design rather
than falling through to unknown. read_report_image accepts the same set.

An empty listing now says which case it is -- no images generated, or images
present under names that did not match -- because a bare zero with error:null
leaves the caller unable to tell the run from the tool. That ambiguity is what
sent the last run off to read the files straight off disk instead.
The startup probe could be satisfied by the PTY's echo of its own source
line, so a session that echoes every line but executes nothing -- the exact
failure the handshake exists to catch -- was accepted as healthy. Assemble
the probe token at runtime, as the completion sentinel already does, so only
real stdout can match it.

Also normalise embedded newlines, not just the trailing one, to CR: an inner
LF strands everything after it in the line editor's buffer for the same
reason a trailing LF did. And let a timeout outrank an error matched in the
partial output, so a command that printed "Error:" and then kept running is
not reported as finished.
…atch

metadata.format was hardcoded to webp even for plain PNG/webp.png files,
and the unrecognized-file hint regex matched extension substrings anywhere
in the filename instead of at the end.
…hake

createSession ran session.start() and verifyResponsive() -- which can take up
to a 15s (now 60s) timeout on a slow cold start -- entirely inside
cleanupLock, so one slow or stuck session stalled every other create/list/
terminate call sharing the lock.

Reserve the session id under the lock, release it, spawn and verify outside
the lock, then take the lock again to publish the result. Reservations
(sessions still mapped to null) now count towards maxSessions alongside
active ones, since concurrent creates would otherwise all see the same
pre-spawn total and admit past the cap together.
…rding them

cols/rows used Number(x) || default, so an explicit caller override of "0"
(or any falsy Number() result) fell through to the hardcoded default instead
of being honored, even though the surrounding code exists specifically to let
caller-supplied env win. Validate for a positive integer instead of relying
on JS falsiness.
Sessions failed for anyone launching an MCP client whose PATH does not
already include openroad (common with GUI-launched clients), since the
server only ever spawned by name and had no fallback. applyInheritedEnv now
runs before settings/logging init: if openroad is not already reachable, it
merges in the login-shell PATH and common install locations
(/opt/homebrew/bin, conda, local OpenROAD builds), without overriding an
explicit PATH that already resolves it.

Updates README, CROSS_PLATFORM, and SECURITY docs to match, and drops the
old "find your paths and edit .env" setup section since the common case no
longer needs it.
Copilot AI lite review requested due to automatic review settings August 16, 2026 06:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov-commenter

codecov-commenter commented Aug 16, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 89.18919% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.39%. Comparing base (f7849cb) to head (37e6233).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
typescript/src/config/path_env.ts 76.59% 18 Missing and 4 partials ⚠️
typescript/src/interactive/session.ts 98.63% 1 Missing ⚠️
typescript/src/tools/report_images.ts 96.00% 1 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@             Coverage Diff             @@
##             main     #175       +/-   ##
===========================================
+ Coverage   67.53%   89.39%   +21.86%     
===========================================
  Files          22       22               
  Lines        1611     1660       +49     
  Branches        0      381      +381     
===========================================
+ Hits         1088     1484      +396     
+ Misses        523      150      -373     
- Partials        0       26       +26     
Flag Coverage Δ
unit 89.39% <89.18%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

The sentinel-based session rewrite replaced manager.ts's sendCommand +
readOutput call pair with a single session.runCommand(), but the
benchmarks/memory_monitoring mock sessions still only implemented the old
methods, so every executeCommand call in those suites threw "runCommand is
not a function" in CI's Docker test stage.
repair_timing returned [ERROR RSZ-0089] with error: null because the
detector only recognised "Error:" / "ERROR:" with a colon. OpenROAD's
native log format has no colon, so a hard tool failure was reported as
success. Match [ERROR …] and [FATAL …] before the generic rules, and
leave [WARNING …] as a non-error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants