Skip to content

fix(sea): run forked helper scripts directly instead of spawning a new session#26366

Open
noxymon wants to merge 6 commits into
google-gemini:mainfrom
noxymon:fix/sea-fork-execpath
Open

fix(sea): run forked helper scripts directly instead of spawning a new session#26366
noxymon wants to merge 6 commits into
google-gemini:mainfrom
noxymon:fix/sea-fork-execpath

Conversation

@noxymon
Copy link
Copy Markdown

@noxymon noxymon commented May 2, 2026

Summary

In the SEA (Single Executable Application) build, child_process.fork(modulePath, args) uses process.execPath as the Node.js interpreter — which is gemini.exe itself. Any fork() call from app code or a transitive dependency therefore launches a second gemini session in a child process instead of executing the requested helper script.

The most visible victim is @lydell/node-pty's WindowsPtyAgent._getConsoleProcessList(), which fork()s conpty_console_list_agent.js during ConPTY teardown. On Windows, this means every run_shell_command invocation in the SEA build can spawn a second gemini session, with debug-log interleaving, lost CLI flags (e.g. --yolo becomes autoEdit), and a node-pty 5-second timeout instead of prompt console-handle reaping.

This PR detects fork-style invocation at SEA entry and runs the requested script directly.

Details

In sea/sea-launch.cjs, before any other startup work in main():

  1. Detect typeof process.send === 'function' — the runtime indicator that this process was spawned with an IPC channel (i.e. via fork()). Note: NODE_CHANNEL_FD is no longer set on the child env in modern Node.js, so process.send is the reliable signal.
  2. Scan process.argv[1..] for the first .js script that is not the binary itself. The SEA inserts a duplicate of execPath at argv[1] (the script-path slot a non-SEA Node.js would have), so for fork(helper.js, [pid]) the SEA child sees argv = [binary, binary, helper.js, pid].
  3. Normalize process.argv to [binary, script, ...remainingArgs] so the helper sees argv in the same positions a regular Node fork would deliver — important for helpers that read process.argv[2] (which conpty_console_list_agent.js does for the shell PID).
  4. Load the script via Module.createRequire(scriptPath). The SEA's built-in require() only resolves built-in modules (Node SEA docs), so createRequire rooted at the script path is required for the helper to load its native-addon dependencies.
  5. On failure, silently return rather than falling through to gemini startup — the parent's IPC timeout will recover, and we never want to spawn a second session.

The fix is generic: it benefits any fork()-using helper, not just node-pty.

Related Issues

Closes #26365

How to Validate

A standalone integration test is included as sea/sea-launch.fork.integration.test.cjs. It mirrors what node-pty does — fork()s a tiny helper script using the SEA binary as execPath and waits for the helper's IPC reply.

# 1. Build the SEA binary
npm run bundle
node scripts/build_binary.js

# 2. Run the integration test against it
node sea/sea-launch.fork.integration.test.cjs dist/win32-x64/gemini.exe

Expected after this PR (PASS):

elapsed: ~30 ms
message: {"status":"ok","receivedShellPid":129984,"runtimePid":...}
RESULT: PASS — fork()'d helper script ran and sent IPC message.

Without this PR (FAIL — bug present):

elapsed: 15007 ms (timeout)
message: null
child-stderr: ...Ripgrep is not available...   ← second gemini session
RESULT: FAIL — fork()'d helper did NOT respond via IPC.

To verify the BEFORE behavior locally, build a binary from main, run the integration test against it, and confirm it fails the same way.

End-to-end on Windows:

  1. Build the binary (above).
  2. Launch gemini.exe --yolo.
  3. Run any shell command (e.g. run shell command: pwd).
  4. Confirm: only one gemini.exe PID exists; GEMINI_DEBUG_LOG_FILE shows a single session; approvalMode stays yolo.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed) — not required (internal SEA bootstrap fix)
  • Added/updated tests (if needed) — sea/sea-launch.fork.integration.test.cjs (integration) and updated sea/sea-launch.test.js
  • Noted breaking changes (if any) — none; the new branch only fires when process.send is set, which is never true for normal user invocations
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run (not affected — bug is SEA-only)
      • npx (not affected — bug is SEA-only)
      • SEA standalone binary (the affected path; verified PASS via integration test, BEFORE/AFTER)
      • Docker
    • Linux
      • npm run (SEA build also affected on Linux in principle; not yet verified)

noxymon added 3 commits April 27, 2026 01:40
Batches printable characters during raw paste events to avoid excessive generator yields that freeze the Ink renderer. Fixes the bracketed paste hang issue described in H1.
…w session

child_process.fork() uses process.execPath as the Node.js interpreter, which
in a SEA build is the gemini binary itself. So fork('helper.js', [pid]) calls
made by helper modules — notably node-pty's _getConsoleProcessList() during
Windows ConPTY cleanup — would launch a full second gemini session instead
of running the requested script. The second session inherits GEMINI_DEBUG_LOG_FILE
and other env vars, runs without the original CLI flags, and on Windows can
even surface a duplicate UI session with approvalMode reset to the default.

Detect this case at SEA entry: when process.send is set (an IPC channel
exists, indicating fork()) and argv contains a .js script path other than
the binary itself, normalize argv to the shape the helper expects
([binary, script, ...args]) and load the script with createRequire so it
can resolve disk-based dependencies that the SEA's built-in require() does
not support.
Spawns a tiny helper script via child_process.fork() with execPath set to
the SEA binary under test, mirroring how node-pty's
windowsPtyAgent._getConsoleProcessList() invokes conpty_console_list_agent.js
during ConPTY teardown. The helper sends an IPC message back; the test
passes if and only if the message is received within the timeout.

Without the fix the SEA spawns a full second gemini session and the IPC
message never arrives (test times out, exits 1). With the fix the helper
runs and replies in tens of milliseconds (test exits 0).

Usage: node sea/sea-launch.fork.integration.test.cjs <path-to-binary>
@noxymon noxymon requested a review from a team as a code owner May 2, 2026 06:44
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue in the Single Executable Application (SEA) build where child_process.fork() calls inadvertently spawn secondary gemini sessions instead of executing the intended helper scripts. By detecting the IPC channel at startup and normalizing arguments, the application now correctly delegates to the helper script. Additionally, the PR includes a performance optimization for the CLI UI to handle large paste operations more efficiently.

Highlights

  • SEA Fork Detection: Implemented a mechanism in sea-launch.cjs to detect when the application is spawned via child_process.fork() and execute the target helper script directly instead of launching a redundant gemini session.
  • Integration Testing: Added a new integration test, sea-launch.fork.integration.test.cjs, to verify that forked helper scripts correctly receive IPC messages when running against the SEA binary.
  • UI Performance Optimization: Optimized KeypressContext.tsx by batching printable characters during raw paste operations to prevent excessive generator yields and UI hangs.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces performance optimizations for large text pastes by batching printable characters and adds logic to correctly handle child_process.fork() calls within Single Executable Application (SEA) builds. Review feedback points out a critical flaw in the keypress batching implementation that could break ANSI escape sequences, such as arrow keys, and suggests using absolute paths in the fork detection logic to ensure consistent behavior across different working directories.

Comment thread packages/cli/src/ui/contexts/KeypressContext.tsx Outdated
Comment thread sea/sea-launch.cjs
noxymon added 3 commits May 2, 2026 13:56
…eck and createRequire

Two robustness improvements raised in code review:

1. process.execPath is always absolute, but argv[i] from a forked invocation
   can be relative (e.g. 'test-binaries/gemini-debug.exe'). String-comparing
   the two would fail to skip the binary's own path duplicate. Resolve the
   candidate to an absolute path with path.resolve before the comparison.

2. Module.createRequire requires an absolute path or file URL — relative paths
   resolve against process.cwd(), which can change mid-startup (e.g. during
   worktree setup). Use the resolved absolute scriptPath for both
   createRequire and the recursive scriptRequire(scriptPath) call.
…-batching dispatcher

The data-listener optimization in createDataListener batches consecutive
printable characters into a single 'paste' event so large clipboard pastes do
not trigger thousands of React re-renders. Two correctness issues:

1. Escape-sequence interleaving: in '\x1b[A' (Up arrow) the bytes '[' and 'A'
   are printable (>= 0x20) and were being collected into a 'paste' batch
   while the parser was mid-CSI, leaving the parser stuck waiting for a
   sequence end and silently swallowing arrow keys, function keys, and other
   ESC-prefixed inputs. Track an inEscape flag plus the intro byte so we feed
   sequence bytes to the parser instead of batching them, and exit escape mode
   on either a parser-emitted event or a sequence-typical final byte (CSI/SS3
   final 0x40-0x7E, OSC BEL/ST). The state persists across data chunks since
   a sequence may straddle them.

2. Single-character / fast-typing inputs were being misclassified as paste:
   any printable run of length > 1 (e.g. typing 'abc' or a 3-char CJK input)
   produced a 'paste' event instead of individual keypress events, breaking
   the keypress contract relied on by tests and consumers. Introduce a
   PASTE_BATCH_THRESHOLD of 32 characters; below it, feed each char through
   the parser so individual keypress events are emitted; at or above it,
   emit the single 'paste' event that solves the actual UI hang case.
@noxymon
Copy link
Copy Markdown
Author

noxymon commented May 2, 2026

Thanks for the review. Both points addressed in e0ab543f4 (sea-launch.cjs) and 42b74eea8 (KeypressContext.tsx).

Comment 2 — sea/sea-launch.cjs absolute path: agreed. Fixed by resolving the candidate via path.resolve before comparing against process.execPath (which is always absolute) and before passing to Module.createRequire (which per Node docs requires an absolute path or file URL — relative paths resolve against process.cwd() which can change mid-startup).

Comment 1 — KeypressContext.tsx batching: confirmed and fixed. Tracing \x1b[A through the previous code: ESC is fed to the parser (parser enters CSI state, i=1); [ is then printable so the dispatcher batches it together with A, emitting a paste event for [A; the parser is left waiting indefinitely for the rest of the sequence and the Up arrow is silently lost. The fix:

  • Track an inEscape flag plus the intro byte ([/O/]/etc.) across data chunks (a sequence may straddle them).
  • While inside a sequence, feed every byte through the parser. Exit on either a parser-emitted key event (works for sequences that produce events) or a sequence-typical final byte — 0x40-0x7E (@-~) for CSI/SS3, BEL/ST for OSC — needed for the parser-ignored cases (focus events, mouse reports) so we don't get stuck.

While fixing this I also found a related correctness bug not in the original review: small printable runs like the test inputs "abc", "你好", "안녕하세요" were also being collapsed into a single paste event because the previous threshold was > 1. The test suite expects per-character keypress events for these. Introduced a PASTE_BATCH_THRESHOLD of 32; runs below it go through the parser one char at a time (preserving keypress semantics for typing); runs at or above it become the paste event that solves the actual UI hang case. All 142 KeypressContext tests now pass.

Verified end-to-end:

  • KeypressContext.test.tsx: 142/142 pass.
  • sea/sea-launch.test.js: 17/17 pass (the one pre-existing failure on recreates runtime if existing has wrong permissions is unrelated).
  • sea/sea-launch.fork.integration.test.cjs against the rebuilt SEA binary: PASS in ~30 ms ({"status":"ok","receivedShellPid":129984,...}).

@gemini-cli
Copy link
Copy Markdown
Contributor

gemini-cli Bot commented May 10, 2026

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

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

Labels

area/platform Issues related to Build infra, Release mgmt, Testing, Eval infra, Capacity, Quota mgmt priority/p1 Important and should be addressed in the near term. status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows SEA: child_process.fork() in SEA build spawns a second gemini session instead of running helper scripts

1 participant