Skip to content

hot: reset process.stdin/stdout/stderr listeners on reload - #30633

Open
robobun wants to merge 16 commits into
mainfrom
farm/5f7289a3/hot-reload-stdin-listeners
Open

hot: reset process.stdin/stdout/stderr listeners on reload#30633
robobun wants to merge 16 commits into
mainfrom
farm/5f7289a3/hot-reload-stdin-listeners

Conversation

@robobun

@robobun robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15027

Problem

  • Under bun --hot, a script using node:readline on process.stdin processes every line (or keystroke) twice after one reload, three times after two, and so on (hheelllloo echo in the issue).
  • GlobalObject::reload() (src/jsc/bindings/ZigGlobalObject.cpp) only clears the module loader and the require map. process and its stdin/stdout/stderr streams are the same objects across reloads, so every load adds its own 'data'/'keypress'/'resize' listeners on top of the previous load's.
  • In a terminal the problem is worse than duplicated listeners: node:readline lives in the internal module registry and is not re-evaluated on reload, so after the listeners are dropped its emitKeypressEvents() still sees its marker on process.stdin and does nothing, which would leave readline silent.

Fix

  • Each stdio stream created by ProcessObjectInternals.ts gets a private @resetStdioForHotReload method. reload() calls Bun::resetStdioForHotReload() (BunProcess.cpp), which invokes it on whichever of stdin/stdout/stderr have already been created (getDirect(), so a never-touched stream is not constructed just to be reset).
  • stdout/stderr: removeAllListeners(). They have no internal listeners.
  • stdin, in order:
    • release the native reader (disown()), and leave raw mode if the previous load entered it;
    • delete readline's keypress-decoder marker so the next createInterface() installs its keypress bridge again;
    • unpipe(), remove 'data' listeners one at a time, pause(), then removeAllListeners();
    • re-register the stream's own resume/pause/end/close handlers (now named functions so they can be re-added) and clear _readableState.reading, the same state onStreamPause leaves behind.
  • A reset that throws is cleared and the remaining streams are still reset; a termination exception stops the loop and reload() returns through the usual RETURN_IF_EXCEPTION.
  • Verified with test/cli/hot/hot-stdin.test.ts:
    • pipe mode: after two reloads stdin still has exactly one 'data' listener and each line is echoed once, by the newest load;
    • terminal mode (PTY driven by hot-stdin-pty.py, skipped on Windows): one data/keypress/resize listener on every load, world echoed once after the reload.
    • Without the src/ changes both tests fail: pipe mode reports LISTENERS 2 2 2 3 0 and echoes from the old load, PTY mode prints wwoorrlldd and two ECHO world lines.
    • hot.test.ts and hot-stdin.test.ts also pass with BUN_JSC_validateExceptionChecks=1; test/js/node/process/process-stdin.test.ts passes.

Background

  • --hot reload: GlobalObject::reload() drops the module registries and re-evaluates the entry point in the same JSGlobalObject. Anything hanging off globals, including process, survives.
  • Internal module registry: Bun's node:* modules are evaluated once per process and cached. Module-level state in node:readline, such as the Symbol("keypress-decoder") it stores on a stream once emitKeypressEvents() has run, therefore survives a reload too. The marker is found by its description rather than imported, because the symbol is private to internal/readline/emitKeypressEvents and requiring that module from the stdin reset would load readline into every --hot process on its first reload. The companion escape-decoder generator is left in place on purpose: emitKeypressEvents() replaces it anyway once the marker is gone, and a still-pending escape-sequence timeout from the old load calls .next() on it.
  • 'data' listeners: Readable.prototype.removeListener('data', fn) clears the stream's kDataListening flag when the last listener goes; removeAllListeners() does not, and instead schedules updateReadableListening(), which would call resume() and re-acquire stdin on behalf of a load that no longer exists. pause() clears kFlowing, so a chunk that was already in flight is buffered for the next load instead of being emitted to nobody. unpipe() is the only call that clears state.pipes.
  • Raw mode: setRawMode(true) turns off terminal signal generation (ISIG); readline's own keypress handler is what turned Ctrl+C into SIGINT. Once that handler is removed, the terminal has to go back to cooked mode or Ctrl+C stops working until the new load happens to enter raw mode itself.
  • Exception scopes: resetStdioForHotReload() declares a ThrowScope, so like any other throwing JSC function its caller has to check for an exception afterwards; reload() does that with RETURN_IF_EXCEPTION, which is also what satisfies BUN_JSC_validateExceptionChecks. Inside the loop, tryClearException() clears ordinary exceptions but refuses to clear a termination, which is how the loop knows to stop.

@robobun

robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:05 AM PT - Aug 15th, 2026

@robobun, your commit 667ee41 has some failures in Build #98234 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 30633

That installs a local version of the PR into your bun-30633 executable, so you can run:

bun-30633 --bun

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a builtin reset identifier and stream helpers, implements a C++ orchestrator invoked during GlobalObject::reload(), and adds a regression test to ensure process stdin/stdout/stderr listeners do not accumulate across hot reloads.

Changes

Stdio listener reset on hot reload

Layer / File(s) Summary
Builtin identifier and C++ declaration
src/js/builtins/BunBuiltinNames.h, src/jsc/bindings/BunProcess.h
Registers builtin private identifier resetStdioForHotReload and declares the C++ bridge function resetStdioForHotReload.
Stdio stream reset implementations
src/js/builtins/ProcessObjectInternals.ts
Adds $resetStdioForHotReload() to write streams and refactors stdin handling to use named handlers, remove accumulated listeners, and reset _readableState.reading.
C++ reset orchestrator
src/jsc/bindings/BunProcess.cpp
Implements Bun::resetStdioForHotReload that calls each stdio stream's reset callback via getDirect() and clears exceptions.
Hot reload integration
src/jsc/bindings/ZigGlobalObject.cpp
Invokes Bun::resetStdioForHotReload(this) early in GlobalObject::reload() before clearing module loader and require map.
Regression test for listener cleanup
test/cli/hot/hot-stdin.test.ts
Adds an end-to-end test running a readline fixture under --hot, triggering reloads and asserting listener counts and single-echo behavior to prevent regression.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically identifies the main change: resetting process stdio listeners during hot reload, which is the core objective of this PR.
Linked Issues check ✅ Passed The PR fully addresses issue #15027 by resetting stdio listeners on hot reload. Changes to stdin/stdout/stderr stream handling in ProcessObjectInternals.ts and the resetStdioForHotReload function in BunProcess.cpp prevent listener accumulation, eliminating the double-echo problem described in the issue.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing listener accumulation across hot reloads. Modifications span builtin names, stream handling, process bindings, hot reload entry point, and a targeted test—all necessary to implement and validate the stdio reset mechanism.
Description check ✅ Passed The description clearly explains the problem, implementation, background, and verification steps, satisfying the repository template requirements.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/cli/hot/hot.test.ts`:
- Around line 754-773: The fire-and-forget async IIFEs that iterate
runner.stdout and runner.stderr can raise unhandled rejections when
runner.kill() aborts the streams; wrap each invoked async IIFE (the ones reading
from runner.stdout and runner.stderr) with .catch(() => {}) so any stream-abort
errors are swallowed silently, e.g., the async () => { for await (const chunk of
runner.stdout) ... }() and the async () => { for await (const chunk of
runner.stderr) ... }() invocations should have .catch(() => {}) appended to
their returned promises to avoid unhandled rejections.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5dac22a1-5f69-4ae3-b637-b956b95217d9

📥 Commits

Reviewing files that changed from the base of the PR and between b9c757b and 434372f.

📒 Files selected for processing (7)
  • src/js/builtins/BunBuiltinNames.h
  • src/js/builtins/ProcessObjectInternals.ts
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/BunProcess.h
  • src/jsc/bindings/ZigGlobalObject.cpp
  • test/cli/hot/hot-stdin-readline-fixture.js
  • test/cli/hot/hot.test.ts

Comment thread test/cli/hot/hot.test.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Duplicate Output Issue After Repeated Saves with bun --hot #13511 - Duplicate output after repeated saves with bun --hot is caused by the same stdio listener accumulation this PR fixes

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #13511

🤖 Generated with Claude Code

Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/js/builtins/ProcessObjectInternals.ts
Comment thread test/cli/hot/hot-stdin.test.ts
Comment thread src/js/builtins/ProcessObjectInternals.ts
Comment thread src/js/builtins/ProcessObjectInternals.ts
@robobun

robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Ready for review. Current head is 667ee41. Its CI run, build 98234, passed every job that ran (177/177: all Linux lanes including ASAN, and both Windows lanes) and ended red only because the darwin 14 aarch64 test shards, currently the only macOS test lane in the pipeline, expired in the queue without ever starting (the automatic retries expired the same way). That agent pool has a CI-wide backlog of several hundred jobs from unrelated builds, so the lane is unavailable to every PR at the moment. So there is no macOS test result for this exact head; the last head that did get macOS coverage (build 67314, before the rebase and the comment/exception-check cleanup in 667ee41) passed both new tests on the darwin 14 lanes. I am not re-pushing to retrigger, since that would only re-queue the same jobs behind the same backlog. All review threads are resolved.

Latest changes

  • Rebased onto main @ 88a6398. Two conflicts, both mechanical: main had deleted the processThrowDeprecation getter/setter that resetStdioForHotReload() was inserted next to in BunProcess.cpp, and added a declaration at the same spot in BunProcess.h. Resolved by keeping main's deletions/additions and placing the new function and declaration after them. Everything else applied cleanly; main's newer getStdinStream (EOF tracking, re-arming of an interrupted internalRead) needed no changes to the reset sequence.
  • 667ee41: reload() now checks the call with the ordinary RETURN_IF_EXCEPTION instead of assertNoExceptionExceptTermination(), and the per-stream loop stops on a termination exception. The stdio reset comments were trimmed to the facts that are not visible from the code; the longer rationale moved into the PR description.

Verification on the rebased branch

  • test/cli/hot/hot-stdin.test.ts: both cases pass with the fix; with src/ at main both fail (pipe mode reports LISTENERS 2 2 2 3 0 and an echo from the stale load, PTY mode prints wwoorrlldd and two ECHO world lines).
  • hot-stdin.test.ts and hot.test.ts pass under BUN_JSC_validateExceptionChecks=1; test/js/node/process/process-stdin.test.ts passes.

CI notes

Fix summary

GlobalObject::reload() invokes a per-stream @resetStdioForHotReload on any already-created process.stdin/stdout/stderr. For stdin it releases the native reader, leaves raw mode, clears readline's keypress-decoder marker, unpipes, clears kDataListening/kFlowing, removes all listeners, and re-adds the stream's own handlers; for stdout/stderr it removes listeners. Details and background are in the PR description.

Comment thread src/js/builtins/ProcessObjectInternals.ts
Comment thread test/cli/hot/hot-stdin.test.ts
@robobun
robobun force-pushed the farm/5f7289a3/hot-reload-stdin-listeners branch from 7f5f61f to 9c9e1f1 Compare May 14, 2026 17:30
@robobun
robobun force-pushed the farm/5f7289a3/hot-reload-stdin-listeners branch from 9c9e1f1 to bc5513f Compare May 26, 2026 05:05
Comment thread src/js/builtins/ProcessObjectInternals.ts
Comment thread test/cli/hot/hot-stdin.test.ts
Comment thread test/cli/hot/hot-stdin-pty.py Outdated
Comment thread test/cli/hot/hot-stdin-pty.py
Comment thread test/cli/hot/hot-stdin-pty.py Outdated
Comment thread src/js/builtins/ProcessObjectInternals.ts
@robobun
robobun force-pushed the farm/5f7289a3/hot-reload-stdin-listeners branch from 4e96927 to d5bf791 Compare June 30, 2026 20:40
In --hot mode the JSGlobalObject and its process.stdin/stdout/stderr
streams survive module-registry reloads. User code (notably
node:readline) that attached 'data'/'keypress'/'error'/'end' listeners on
the previous evaluation remained attached, so after the entry point was
re-run a second set of handlers stacked on the same stream. Every
keystroke was then handled by both the old and new readline instances,
producing doubled (then quadrupled, etc.) echo in the terminal.

On reload, drop user-attached listeners from stdin/stdout/stderr and
re-add the internal own()/disown() wiring so the stream is clean for the
fresh evaluation.

Fixes #15027.
hot.test.ts has a pre-existing timing-sensitive test that fails on
debug builds; keep the stdin-listener coverage in a dedicated file so
it can be run in isolation. Fixture is inlined via tempDir().
resetStdioForHotReload() declares a ThrowScope whose destructor
simulates a throw on return; without an intervening check the next
scope constructed by requireMap()->clear() trips
validateExceptionChecks=1 on the asan lane.
removeAllListeners() leaves the Readable's kDataListening bit set and
schedules updateReadableListening() on the next tick, which would call
self.resume() → own() and undo the disown() in the reset. It also
leaves kFlowing set, so a chunk pushed by an in-flight internalRead
before the new load attaches its handler would be emitted to zero
listeners instead of buffered.

Remove 'data' listeners individually (clears kDataListening) and
pause() (clears kFlowing) before removeAllListeners().
- unpipe() before removeAllListeners() so _readableState.pipes from the
  previous load isn't pinned across reloads.
- The Windows file watcher can fire multiple events for a single
  writeFileSync, so one reload trigger may advance the load counter by
  more than one. Rework the test to assert the invariants (listener
  counts stay at 1; exactly one ECHO per input from a post-reset load)
  without pinning exact load numbers.
readline sets raw mode (ISIG cleared). Once the reset strips the
keypress handler that translated ^C, Ctrl+C is dead if the new load
doesn't re-enter raw mode itself. Restore cooked mode so the terminal
behaves after a reload that stops using readline.
node:readline is cached in the InternalModuleRegistry and is not
re-evaluated on --hot reload, so its module-local KEYPRESS_DECODER /
ESCAPE_DECODER symbols keep pointing at properties left on
process.stdin. emitKeypressEvents() early-returns when it sees them, so
after the reset removed the data→keypress bridge it was never
reinstalled and terminal-mode readline went silent after a reload.
Delete those properties in the reset so the next createInterface()
wires keypress events up again.

Add a PTY-driven test covering the terminal path: asserts one
data/keypress/resize listener on each load, exactly one echo of the
pre-reload line, and exactly one echo of the post-reload line from a
post-reload interface.
macOS/BSD report PTY EOF as an empty read instead of EIO; break out of
the wait loop in that case so a failing run doesn't spin on the closed
fd until the deadline.
The hand-rolled fork doesn't give the child a controlling TTY, so
closing the PTY master won't SIGHUP it. Handle SIGTERM/SIGINT (test
runner timeout, Ctrl+C) like SIGALRM and kill the child explicitly so a
failing run doesn't leak a bun --hot process.
READY 2 is only printed after createInterface() has synchronously
re-wired the data→keypress bridge, so waiting for it is sufficient;
typed input is kernel-buffered regardless.
The emitKeypressEvents() idempotence guard checks only KEYPRESS_DECODER;
ESCAPE_DECODER is overwritten unconditionally once the guard passes.
Deleting the escape decoder was unnecessary and could turn a pending
escape-sequence timer from the previous load into an uncaught TypeError
if the next load doesn't re-enter terminal readline.
main now installs an auto-destroy 'end' handler on process.stdin, so the
expected baseline is data=1 error=1 end=2 resize=0.
@robobun
robobun force-pushed the farm/5f7289a3/hot-reload-stdin-listeners branch from d5bf791 to 200b431 Compare August 15, 2026 14:37
Comment thread src/js/builtins/ProcessObjectInternals.ts Outdated
Comment thread src/js/builtins/ProcessObjectInternals.ts Outdated
Comment thread src/js/builtins/ProcessObjectInternals.ts Outdated
Comment thread src/js/builtins/ProcessObjectInternals.ts Outdated
Comment thread src/jsc/bindings/BunProcess.cpp Outdated
Comment thread src/jsc/bindings/BunProcess.cpp Outdated
Comment thread src/jsc/bindings/ZigGlobalObject.cpp Outdated
Comment thread src/jsc/bindings/ZigGlobalObject.cpp Outdated

@claude claude Bot 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.

I reviewed the current state of this PR (post-rebase onto 3e08719) and found no new issues. Given the number of subtle interactions here — stream kDataListening/kFlowing state, TTY raw-mode restoration, the readline KEYPRESS_DECODER marker surviving InternalModuleRegistry, JSC exception-scope simulation in GlobalObject::reload() — a human look is still worthwhile before merge.

What was reviewed:

  • The stdin reset sequence in ProcessObjectInternals.ts against node:readline and Readable-stream internals (per-listener removeListener('data') clears kDataListening; escape-decoder intentionally left in place so a pending escape timer doesn't throw).
  • resetStdioForHotReload() in BunProcess.cpp: getDirect() avoids reifying lazy streams; CLEAR_IF_EXCEPTION per iteration; the assertNoExceptionExceptTermination() in ZigGlobalObject.cpp before the locked clearAll().
  • Both new tests (pipe-mode listener counts now 1 1 2 0 after the internal end handler landed on main; PTY driver's EOF/signal handling and the removed sleep(0.3)).
  • The comment-cop flags on the multi-line comments were examined and ruled out — they document non-obvious stream-state and InternalModuleRegistry behaviour, not workarounds.
Extended reasoning...

Overview

This PR fixes #15027: in --hot mode, process.stdin/stdout/stderr survive module-registry reloads, so listeners attached by the previous evaluation (notably node:readline) stack up and every input byte is processed N+1 times after N reloads. The fix adds a private $resetStdioForHotReload builtin on each stdio stream and calls it from GlobalObject::reload() via a new Bun::resetStdioForHotReload() C++ helper that iterates only already-reified streams (getDirect()). Seven files: one builtin-name addition, the JS reset logic in ProcessObjectInternals.ts, the C++ dispatch in BunProcess.cpp/.h, the call site in ZigGlobalObject.cpp, and two new test files (a pipe-mode TS test plus a Python PTY driver for terminal mode).

Security risks

None identified. The reset function is stored on a private builtin name (unreachable from user code), and resetStdioForHotReload() is only called from the --hot reload path. The PTY test driver forks/execs the debug bun binary in a temp dir with no network or privilege escalation.

Level of scrutiny

High. This touches the --hot reload path in ZigGlobalObject.cpp, JSC exception-scope discipline (a prior round of this PR tripped validateExceptionChecks), and the getStdinStream builtin whose closure state (reader, forceUnref, needsInternalReadRefresh, stream_destroyed) interacts with Readable's kDataListening/kFlowing machinery and TTY raw mode. The PR has already been through ~13 rounds of automated review that surfaced real issues (missing unpipe(), missing setRawMode(false), the KEYPRESS_DECODER early-return, the escape-decoder timer TypeError, PTY EOF spinning on macOS, orphaned --hot children on SIGTERM), all of which were addressed. That history is exactly why a human should confirm the final composition rather than auto-approving.

Other factors

All prior inline findings from this reviewer are marked resolved with corresponding fix commits. The only unresolved thread is today's comment-cop bot flagging multi-line comments in ProcessObjectInternals.ts/BunProcess.cpp/ZigGlobalObject.cpp; the bug-hunting system examined and refuted these — the comments explain non-obvious behaviour (why removeAllListeners() alone would let updateReadableListening re-own(), why escape-decoder must not be deleted, why getDirect() is used) rather than justifying a workaround, which is what CLAUDE.md rule #13 targets. The rebase notes indicate a recent conflict resolution with a cellLock() guard and a new internal end listener on main; the test's expected end=2 count and the ordering of resetStdioForHotReload() before the locked clearAll() block look correct for that.

…sserting

reload() now uses the ordinary RETURN_IF_EXCEPTION after the call, and the
per-stream loop stops early on a termination exception. Trim the stdio reset
comments down to the facts that are not visible from the code.
Comment thread src/js/builtins/ProcessObjectInternals.ts
Comment thread src/js/builtins/ProcessObjectInternals.ts

@claude claude Bot 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.

I reviewed this PR again after the rebase and comment-trimming pass — no bugs found. Because it introduces a new hot-reload/stdio coupling (private @resetStdioForHotReload on each stream, invoked from GlobalObject::reload()) with subtle interactions across readline internals, TTY raw mode, and Readable stream state, a human look at the overall approach would still be worthwhile.

What was reviewed:

  • The new tryClearException() loop in resetStdioForHotReload — returns false only on termination, so normal exceptions are swallowed per-stream and termination propagates; RETURN_IF_EXCEPTION in reload() satisfies the exception-scope validator.
  • The end=2 baseline update — matches the new internal onStreamEnd handler, which the reset re-registers so the count stays fixed across reloads.
  • The remaining comment-cop flags on lines 343/351 — those are 2–3-line explanations of cross-module invariants (InternalModuleRegistry caching, kDataListening), not workaround justifications; I don't think they need further trimming.
Extended reasoning...

Overview

This PR fixes #15027 by adding a private $resetStdioForHotReload builtin to each of process.stdin/stdout/stderr, invoked from GlobalObject::reload() before the module registry is cleared. For stdin the reset disowns the native reader, restores cooked TTY mode, deletes readline's keypress-decoder marker, unpipes, removes data listeners individually (to clear kDataListening), pauses, strips all listeners, and re-registers the four internal handlers. For stdout/stderr it just calls removeAllListeners(). Changes span ProcessObjectInternals.ts, BunProcess.{cpp,h}, ZigGlobalObject.cpp, BunBuiltinNames.h, plus a two-case test (pipe + PTY).

Since my last round on 2026-05-26 the branch was rebased and picked up two changes: (1) the pipe-mode test's expected end count moved from 1 to 2 to account for main's new internal auto-destroy end listener, and (2) the C++ side switched from CLEAR_IF_EXCEPTION + assertNoExceptionExceptTermination to a per-stream if (!scope.tryClearException()) return; with RETURN_IF_EXCEPTION in the caller — a strict improvement that lets termination stop the loop while normal exceptions on one stream don't block the others.

Security risks

None identified. The reset is only reachable from --hot reload on the main thread; it uses getDirect() on the process object so it never triggers user getters, and looks up the reset function via a private builtin name, so user code can't intercept or replace it. setRawMode(false) restoring cooked mode is a hardening step (keeps ^C alive), not an exposure.

Level of scrutiny

Medium-high. The change is not large in LOC but is dense in cross-cutting invariants: readline's module-local symbols surviving via InternalModuleRegistry, the Readable kDataListening/kFlowing state machine, TTY raw mode and ISIG, JSC exception-scope discipline, and --hot's partial-teardown semantics. Each of these produced a real bug during earlier review rounds (silent terminal after reload, escape-timer TypeError, PTY spin on macOS EOF, leaked --hot grandchild on test-runner timeout) — all now fixed and covered by the two tests. That history argues the mechanism deserves a maintainer's eye on whether @resetStdioForHotReload is the right layer for this cleanup versus, say, clearing more state in reload() itself.

Other factors

All ten prior inline threads from this reviewer are resolved with corresponding commits. No human reviewer has looked at the PR. The two unresolved comment-cop flags (github-actions) are heuristic style nags on comments that were already trimmed once in 667ee41; the surviving comments explain non-local invariants rather than justify workarounds, so I would not treat them as blocking. The tests are condition-driven (no arbitrary sleeps remain), skip on Windows for the PTY case, and reap the --hot grandchild on SIGTERM/SIGINT/SIGALRM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--hot mode caused double echo in console

1 participant