Repository navigation
feat(run): entrypoint terminal modes: heph's PTY by default, --raw for fidelity - #524
Open
raphaelvigee wants to merge 31 commits into
Open
raphaelvigee wants to merge 31 commits into
raphaelvigee wants to merge 31 commits into
Conversation
raphaelvigee
force-pushed
the
raphaelvigee/entrypoint-terminal-modes
branch
from
October 31, 2026 14:45
a5e6b4a to
a853179
Compare
Member
Author
…eground spawn Adds RunStdio::Pty, moved once from the host's handover to the spawn, with its slave crossing the plugin seam by number (ABI 0.17.2, a distinct RUN_STDIO_PTY so an old plugin refuses it), and a stop/continue pair of frames. pty.rs moves to crates/proc with CLOEXEC ends and one raw-mode owner. A build target's --shell drops its log and redaction and follows a live resize. Nothing picks the PTY path yet: run.rs still inherits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
should_use_tui tests fd 2 and disqualifies only when tcgetpgrp(2) succeeds and differs from our group, so a piped stdin keeps the TUI and `heph run //x &` no longer goes raw before the handover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PtySession opens the pair, makes the caller's terminal raw and pumps stdin to the master and the master to fd 1 with one fixed buffer per direction, so a slow fd 1 backs up into the tool rather than into memory. It answers the tool's stops in the spec's order, and finish() drains under a limit and restores last. The stop raises SIGSTOP rather than kill(getpid()): on Linux kill can return on a worker thread before the stop lands, and that return is the resume. Reasoned from the kernel's behaviour, not measured. raw_terminal_restores_what_it_captured was red on macOS: BSD sets PENDIN itself when ICANON comes back on. It is state rather than a setting, so it is masked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s PTY heph now decides at the handover. A run on a terminal it is the foreground job of, whose driver offered the PTY, takes the pump; everything else inherits, and each run says why in a debug! under the target `entrypoint_mode`. A `--shell` always takes the PTY, so its wrapper streams go. A build target's `--shell` follows live window size changes through a watch channel. `finish` runs before the pause guard drops, so the terminal is cooked again before the TUI resumes or an error renders. The `--raw` flag lands here because the mode rule reads it. On an entrypoint it already takes the inherited path; the next commit gives it its display. entrypoint_kill_under_a_terminal now passes `--raw`: it pins the shared group's X5 limit, which the spec keeps for `--raw` and which a default run no longer has. exec's `--shell` tolerates having no terminal to copy termios from, as the pump does. ABI_SEMVER_C said 0.17.1 where ABI_SEMVER said 0.17.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No TUI, and the default log filter drops to warn, which is all the CI view needs to go quiet: it prints only info! on a healthy run. The logger starts before the real parse, so the soft parse moves ahead of it; a look through argv would take `-- --raw` for heph's. HEPH_LOG still has the last word. On a build target it is a host-side wrapper that never crosses the seam: output through the driver's tee, so redacted as in log.txt, and no stdin, or two runs piped different bytes would share a hashin. A cache hit prints nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The harness gains an interactive shell and a job-control script above heph, resize, terminal close, termios, and `HEPH_PTY_OPEN_FAILS` for the one failure nothing else can provoke. default_run_output_complete, failing_tool_output_is_complete and redirected_run_is_byte_exact are red on real bugs still to fix. stop_during_drain_is_ignored and default_run_drain_is_bounded are ignored: macOS revokes the slave when the tool exits, so no drain window exists there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`heph run //x < file` and `… | heph run //x` with stderr on a terminal failed with "The cursor position could not be read": the tool never ran, which read as empty stdin. Nothing consumed fd 0. With stdin not a terminal crossterm reads /dev/tty, and macOS kqueue refuses that with EINVAL (measured), which `position_to` swallows until its timeout. `use-dev-tty`, on macOS only, swaps crossterm's mio source for one polling with select. Semantics stay uniform; Linux's epoll accepts /dev/tty. The limit is select's: a terminal fd at 1024 or above fails, and the source opens at TUI start, far below it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hell Job control has already made `timeout` the leader of the foreground job at an interactive shell, so heph stays in the foreground and takes the PTY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS has no drain window, measured with a plain PTY and no heph: when a session leader exits, the master reads EOF within milliseconds, while its HUP-ignoring child stays alive and its writes fail with EIO. A slave that is not the controlling terminal reaches EOF only when the child exits. So every OS asserts that the child is alive, that heph leaves within the limit plus a margin, and the tool's status. The limit itself and the warning are asserted on Linux alone. The fixture exits only once the child has written its pid, and the child survives a failing `echo`: with the driver's `-e`, EIO would have ended it, and the HUP could beat its `trap`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ine endings - every_line_arrived tolerates `\r+\n` on macOS only. XNU emits `\r\r\n` every 4095 bytes once a PTY slave's queue fills, measured on a plain PTY with no heph involved. Linux keeps the exact `\r\n`. - local_runner_shell_keeps_working and build_target_shell_forwards_resize: typed stdin and a live resize reach a `--shell` tool, with and without a wrap runner. - cdylib_entrypoint_default_run_gets_slave, cdylib_raw_inherits_fds and cdylib_tool_stopped_resumes_on_fg: the first real crossing of the PTY slave and the stop/continue frames over the plugin seam. The test cdylib now reports its `tty` to a separate file and has a mode that stops itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`printf B | heph run //x`, with stderr a terminal and no `--raw`, went through the build path's interactive wrapper, which handed fd 0 to the action. Stdin is no input of the cache key, so the second run got the artifact of the first. A plain build's wrapper now passes stdin only when fd 0 is a terminal, and a `--shell` as before. Typed keystrokes stay as they are: that hole is reserved for the user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
heph leading its session or otherwise orphaned (`ssh -t host heph run //x`, `tmux new heph ...`) stopped itself with nobody to resume it, and hung. A fourth check before a stop: the parent is in heph's session. An error counts as no. On failure heph skips the stop and continues the tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wrapper in heph's group (`/bin/sh -c 'heph run //x; echo after'`, `make`) never stops with it, so the shell never saw a stop and the prompt never came back. heph now skips the stop unless it leads its group, and continues the tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SIGTTOU is ignored, so a `tcsetattr` from the background succeeds: heph woken by `bg` overwrote the shell's termios on its way to stopping again, and so did a run ending there. `RawTerminal::restore`, and with it `Drop` and every stop, now acts only while heph is the foreground job. A driver's `--shell` no longer goes raw from the background either, as nothing would undo it. The comment in main.rs claimed these calls fail with EIO. `tcsetattr` succeeds; only a read fails. A restore now needs a controlling terminal, so RawTerminal's test runs in a session of its own, and the pump's test leaves the restore to `default_run_restores_the_terminal`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`kill -STOP <heph>`, `bg`, `fg` killed the keyboard: a stdin read from the background returns EIO, which ended stdin for good. A read error while heph is not the foreground now goes through the stop path, as the spec's stops do, and stdin comes back with `fg`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tool can be continued between the `WNOWAIT` wait that reported the stop and the wait that consumes it. Sending `Stopped` then stopped heph for a stop that was already over. It now goes out only when the consuming `waitid` reported a child. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A panic that skips `RawTerminal`'s `Drop` left the terminal raw. The session now registers a restore while raw, and the panic hook runs it. Scoped, so the TUI's closure is back for the second Ctrl-C once the session ends. A panic does not run the TUI's own: it says "aborted (second Ctrl-C)", and the hook already undoes the TUI's raw mode. Fatal signals (SEGV, BUS) and an abort across the plugin seam stay uncovered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R22: nothing is teed from a run on the caller's terminal, so `writes_log` is true for a captured run only, with no file and no artifact on `Inherit`. Also words the PTY arm's drain warning for what it is, as a session has no log tail to truncate, and says on `Entrypoint`'s `Hash` derive that `offers_pty` is no part of what a driver may hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each chunk of output paid two trips to the blocking pool, a write and a flush. A flush now waits while the master has more to read, and is immediate once it has none, so a prompt is not held back. The output buffer grows to 64 KiB; stdin keeps 8 KiB. The stop path and the drain still flush as soon as all they hold is written, because a tool that keeps writing would otherwise put the flush off past their bound. 1 GiB to a PTY on darwin/arm64, interleaved, n=3: 31.93 s before, 20.83 s after, against 8.31 s for --raw. Linux not measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS now bounds doubled line endings rather than trimming them all, the drain warning must carry a non-zero byte count, and a third TERM delivery fails. The stop-during-drain test drops a wait that was no post-signal barrier and says what macOS can prove. raw_requested no longer splits no_config_tolerable from its doc comment, and a --raw after the separator does not quiet the log. The ABI fd tests start at 200, below a macOS soft limit of 256. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin that fails before decoding the run's options never takes the slave, which then stays open until heph exits. The comment said otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lost background output, `\r\r\n` on macOS, seeing the mode under --raw, a self-stopping tool that keeps running with no shell to resume heph, and reset after a fatal signal. --help gains the mode, reset and --shell's output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getppid returns 0 there, and getsid(0) answers for heph itself, so under docker exec -it the session check passed and heph stopped itself for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
local_runner_shell_keeps_working failed once on darwin/arm64 with `^C` as the last output and no prompt within 60s. The log's capture looks cut at the help text only because stderr is interleaved; the session got as far as the Ctrl-C. A bash in a `sleep` loop with an INT trap sometimes ignores a Ctrl-C and loops on. On a bare PTY with no heph, bash 3.2 and 5.3 hung on 11/500 and 8/300 with the Ctrl-C's timing spread over one nap, and a second Ctrl-C ended the loop. With that spread added to the test, it failed 8/300 under a runner and 13/300 without one, so the wrap runner is not the cause. With the resend, 1000/1000 passed. `sleep & wait` is not an alternative: it left /bin/bash 3.2 spinning on the CPU (14/500). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hook also fires on panics tokio catches, so it could leave the terminal cooked under a tool that was still running. An uncaught panic now relies on `RawTerminal`'s `Drop`, since release unwinds. A panic that aborts leaves the terminal raw until `reset`, which docs/ENTRYPOINTS.md now says. The scoped restore stays for `hard_abort`: two presses accepted just before the handover can still reach it with the session raw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The piped-stdin test dropped an assertion a TUI run could never fail, since it never prints a successful action's output, and gives each piped value its own workspace so each can fail alone. A new unit test pins that a burst of chunks does not cost a flush per chunk. Records the measured Ctrl-C loss behind interrupt_to_prompt's resend, and names the commands behind the self-stop limit in the docs, adding a closed terminal and an exited parent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The signal these tests send lands as the resumed tool writes. bash's trap handlers do not restart, so `echo` can fail with EINTR, and the driver's `-e` turned that into the tool's exit, status 1, with heph following it. The barrier then hit ESRCH. Measured on the linux/arm64 CI artifact under load: 7 failures in 1040 runs, each with `echo: write error: Interrupted system call` where inspected. With `set +e` in the fixture: 0 in 800. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
raphaelvigee
force-pushed
the
raphaelvigee/entrypoint-terminal-modes
branch
from
November 1, 2026 09:31
438dc90 to
bd76a8f
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #516. Spec: https://claude.ai/artifact/SzmPFWNomk87UJVGSQy4dw
On a terminal,
heph run //xnow runs an entrypoint behind a PTY heph owns, with no log and no redaction.--rawdrops the TUI and heph's wrapping, and the tool inherits the caller's fds. A redirected or background run inherits too, so pipes stay byte-exact. On a build target,--rawonly changes the display, and a build target's--shellloses its log and redaction and follows a live resize.HEPH_LOG=entrypoint_mode=debug heph run //xsays which mode a run took, and why.Release note
heph runon an entrypoint now runs the tool behind a PTY heph owns when stdin, stdout and stderr are all your terminal and the run is the foreground job. Ctrl-Z does nothing on a plain tool there.--rawis the new way out: no TUI, the tool inherits your fds and gets real Ctrl-Z, with two signal limits; any redirected or background run inherits too. On a build target,--rawonly prints the action's output plainly. Credentials: no entrypoint run and no build target's--shellis logged or redacted, soenvin a build target's--shellnow shows tokens that used to read[redacted]. Stdin: piped or< filestdin no longer reaches a plainheph runbuild, because it was not part of the cache key and could poison an artifact. Seedocs/ENTRYPOINTS.md.Verification
CI runs under
ci/force-cibecause this behaviour is platform-specific.plain_tool_under_ctrl_z_keeps_running,tool_handling_sigtstp_sees_itandself_sigstop_suspends_and_resumesran and passed onlinux/amd64andlinux/arm64(run 41211774247), so Linux matches the macOS spike.code-qualityBLOCKED: heph could stop itself with nobody to resume it (ssh -t,tmux,docker exec). Fixed; round 2 found thedocker exechole, fixed with a unit test and no further round.hermeticityNOT HERMETIC: piped stdin reached a cached build on macOS, through the macOS TUI fix. A plain build now gets stdin only when fd 0 is a terminal; round 2 HERMETIC WITH EXEMPTIONS (typed keystrokes, pre-existing).feature-qualityPASS WITH FOLLOW-UPS,compatibilityCOMPATIBLE,product-visionSHIP WITH CHANGES; applied.code-qualityPASS,compatibilityCOMPATIBLE,product-visionSHIP,feature-qualityPASS WITH FOLLOW-UPS (minors applied in 5b98e22, not re-reviewed).docker exec, measured on linux/arm64 in a container:PPid: 0confirmed. A tool that stops itself is continued, with heph as the direct exec'd command. Exit code and termios were captured only with ansh -cbetween them, and no pre-fix build was compared.--raw8.3 to 9.2 s; by hand 8.9 s. So--rawadds nothing per byte, and the default costs about 2.3x. Warmtrue:--raw18.3 ms, default 20.6 ms, by hand 2.8 ms. Warmgopls version, no tty:--raw32.7 ms, feat: entrypoints — invoke a target in your world through the driver's ordinary run #516 32.5 ms (within noise), by hand 14.8 ms. Linux/arm64 in an OrbStack VM, 1 GiB to a PTY, n=5, so only the ratios transfer: default 8.45 s,--raw4.28 s, by hand 4.81 s. Default is about 2.0x--rawthere, and--rawmatches by hand within noise. Not measured on real Linux hardware.local_runner_shell_keeps_workingfailed once ondarwin/arm64. bash can lose a Ctrl-C; reproduced with added jitter on a bare PTY with no heph (19/800). The test now resends it, 1000/1000 jittered, so it would no longer catch a one-off dropped Ctrl-C.Per-platform differences, for the user to accept or not
default_run_drain_is_boundedpasses with the limit and the warning's byte count asserted: heph waits up to the limit and warns.stop_during_drain_is_ignoredhas a window only on Linux too. Avoiding this would need a helper process in the session.\r\r\non macOS. XNU emits one every 4095 bytes once a slave's queue fills, on a plain PTY with no heph involved. It renders the same. The two output-complete tests collapse it on macOS only; Linux keeps the exact comparison. Clamping the baud rate would remove it but lie to tools, so that was not done.use-dev-tty, macOS only. kqueue rejects/dev/tty, so a redirected stdin killed the TUI and the tool never ran. Same behaviour on both OSes by a different implementation.Departures from the spec
raise(SIGSTOP), notkill(getpid(), SIGSTOP), in Stops step 4. "That call returning is the resume" needs the stop to land before the return; on Linuxkillmay return on a worker thread first. Reasoned, not measured.pty_fallback_warns_when_plugin_offers_noneis inrun.rsandcrates/e2e/tests/entrypoint.rs, not plugin-exec: the fallback is host and engine logic.Driver::supports_shellis a new defaulted trait method, so the engine can gate on the driver that spawns.HEPH_PTY_DRAIN_MS, as the spec's "tests can inject the limit" needs; a test-onlyHEPH_PTY_OPEN_FAILS; a cdylib stop/fgtest.entrypoint_kill_under_a_terminalnow passes--raw, as the spec says: its limit belongs to the shared group.timeoutat an interactive shell: job control makes it a group leader and the foreground job, sotimeout 5 heph run //xunderbash -itakes the PTY path. Under a script it inherits, as the spec says. Docs state which.Open items settled by implementation
DRAIN_DEADLINE, 500ms.kill -TSTP <heph>: takes the same stop path. The handler lasts for the process, so one after the tool exits does nothing.--shellwith no foreground terminal: never raw, and a stop just continues the tool.Piped stdin reaching EOF only stops the pump; no VEOF is sent, so a piped
--shelltool may wait. The spec is silent on this, and the old pump did the same.Left for the user (in the spec's Open)
oci.execvespec.Known CI problem, not from this PR
Test linux/arm64can fail inplugingo-e2ewithNo space left on device. It failed three times at 438dc90 and passed at 5b98e22 with the same code for that job; the head commit touched only abin-e2efixture. The user sent this to a separate session.Decided by the user, 2026-10-31
feat: entrypoints — invoke a target in your world through the driver's ordinary run #516's SIGKILL race is accepted: heph killed between spawning a tool and registering it orphans the tool. The test now waits until the tool is tracked (0d085a7, on feat: entrypoints — invoke a target in your world through the driver's ordinary run #516).
Accepted: macOS late daemon output lost with no warning; macOS
\r\r\non long PTY output; a fatal signal to heph (SEGV, BUS) or an abort across the plugin seam leaving the terminal raw untilreset.The panic hook does not restore a PTY session's terminal, because it would also fire on panics tokio catches and cook the terminal under a live tool. Uncaught panics rely on
Drop; a panic that aborts leaves the terminal raw untilreset.🤖 Generated with Claude Code