Skip to content

fix(cli): efficient shell output streaming#20406

Open
jacob314 wants to merge 2 commits intomainfrom
memory_usage3
Open

fix(cli): efficient shell output streaming#20406
jacob314 wants to merge 2 commits intomainfrom
memory_usage3

Conversation

@jacob314
Copy link
Contributor

@jacob314 jacob314 commented Feb 26, 2026

Summary

Optimize shell output support to avoid memory usage crashes and improve performance by 30X

Before demo on the same file:
https://screencast.googleplex.com/cast/NTE1NTk3NTcyMjk1ODg0OHxkMjE4ODdjNy0xYg
Performance is so slow you may not run out of memory because the heat death of the universe may occur first.
After demo:
You can now read a 5GB file streamed to stdout and not crash. Performance is now generally aligned with VSCode for the same task instead of being 30X slower.
https://screencast.googleplex.com/cast/NjUyMTA1MjIyNTA3NzI0OHw5YTZiMGIwNS1jNg

Fixes #18953

How to test

Test to make sure this hasn't regressed any crucial use cases related to streaming shell output. In particular make sure content passed to the model is still behaving as you would expect. Ensure the saved file still has the full content for very large outputs.

fix(cli): Write shell command output to a file and limit memory buffered in UI

Fixes.

Checkpoint.

fix(core, cli): await outputStream.end() to prevent race conditions

This commit fixes a critical race condition where
was called synchronously without being awaited. This led to potential file
truncation or EBUSY errors on Windows when attempting to manipulate the file
immediately after the  call.

Additionally, this change removes fixed wait times (`setTimeout`) that
were previously used in test files as a band-aid.

fix(core): stream processed xterm output to file to remove spurious escape codes

test(core): update shell regression tests to use file_data events
@jacob314 jacob314 requested a review from a team as a code owner February 26, 2026 08:34
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @jacob314, 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 significantly overhauls how shell command outputs are handled, particularly for large data streams. The primary goal is to enhance stability and performance by offloading extensive output from memory to temporary files, thereby preventing UI freezes and memory exhaustion. It also refines the output processing to ensure clean, readable logs and introduces robust error handling and cleanup for these temporary files, making the system more resilient and user-friendly for long-running or verbose commands.

Highlights

  • Efficient Shell Output Streaming: Implemented a mechanism to stream large shell command outputs directly to a temporary file, significantly reducing memory buffering in the UI and preventing performance issues or crashes for extensive outputs.
  • Race Condition Prevention: Addressed a critical race condition by ensuring outputStream.end() is properly awaited, which resolves potential file truncation or EBUSY errors, especially on Windows, when files are accessed immediately after writing.
  • Cleaned Output for File Storage: Processed xterm output before writing to files, effectively removing spurious escape codes and ensuring that saved logs contain clean, readable text.
  • Improved UI Responsiveness: Limited the amount of shell output buffered in the UI to 100KB, ensuring that the user interface remains responsive even when very large outputs are being generated in the background.
  • Enhanced Truncation Handling: Updated the tool output truncation logic to save the full output to a file when exceeding a threshold, and to reference this file in the truncated message presented to the user and the model.
  • Test Suite Enhancements: Added new integration tests to validate the large output streaming functionality and the correct resolution of carriage returns and backspaces in the processed output. Removed fixed wait times (setTimeout) from tests, replacing them with more robust asynchronous handling.
Changelog
  • integration-tests/run_shell_command_file_stream.test.ts
    • Added new integration tests to verify streaming of large shell command outputs (20MB and 50MB) to files, ensuring full content presence.
    • Included a test case to confirm that processed output correctly resolves carriage returns and backspaces, and strips ANSI escape codes.
  • packages/cli/src/ui/components/AnsiOutput.tsx
    • Changed React import from type React to React to align with new React 18 JSX transform.
    • Memoized the AnsiLineText component to optimize rendering performance.
  • packages/cli/src/ui/hooks/shellCommandProcessor.test.tsx
    • Updated mock configuration to include getTruncateToolOutputThreshold, storage, and getSessionId for testing purposes.
    • Mocked fs.createWriteStream to simulate file writing behavior during tests.
  • packages/cli/src/ui/hooks/shellCommandProcessor.ts
    • Imported moveToolOutputToFile for handling file-based output.
    • Removed MAX_OUTPUT_LENGTH constant and made truncation threshold configurable.
    • Implemented file streaming for shell command output by creating a temporary log file and writing raw_data events to it.
    • Limited the cumulativeStdout buffer for UI display to 100KB to prevent memory spikes.
    • Ensured outputStream.end() is awaited before proceeding to prevent race conditions.
    • Integrated moveToolOutputToFile to save full command output to a persistent location if truncation occurs, updating the displayed warning message with the file path.
    • Modified addShellCommandToGeminiHistory to accept a maxOutputLength parameter for more flexible truncation in history.
    • Added cleanup logic to destroy the outputStream and delete the temporary output file if it was not moved due to truncation.
  • packages/cli/src/ui/hooks/shellReducer.ts
    • Added a MAX_BG_OUTPUT_LENGTH limit (100KB) to background shell output to prevent excessive memory usage in the UI.
  • packages/core/src/scheduler/tool-executor.test.ts
    • Imported fsPromises for file system operations in tests.
    • Mocked moveToolOutputToFile in fileUtils.
    • Added new test cases to verify truncation and file movement when fullOutputFilePath is provided by the tool.
    • Included tests to ensure temporary files are correctly deleted when output is not truncated, or when an error or abort occurs during tool execution.
  • packages/core/src/scheduler/tool-executor.ts
    • Imported fsPromises and debugLogger.
    • Integrated fullOutputFilePath handling: if provided, the executor now checks if truncation is needed and uses moveToolOutputToFile.
    • Ensured temporary fullOutputFilePath files are unlinked on abort or error conditions.
    • Modified createSuccessResult to include outputFile in the function response if available.
  • packages/core/src/services/shellExecutionService.test.ts
    • Added lastCommittedLine: -1 to the ActivePty mock initialization.
    • Adjusted binary output expectations in tests to reflect that only the sniffed binary chunk is returned, not the full buffer.
  • packages/core/src/services/shellExecutionService.ts
    • Introduced new ShellOutputEvent types: raw_data (raw output before PTY render), file_data (processed text for file logs).
    • Added lastSerializedOutput and lastCommittedLine to ActivePty interface to track committed lines for file streaming.
    • Implemented emitPendingLines function to stream processed terminal lines to file_data events, handling wrapped lines and trimming.
    • Refactored ActiveChildProcess to remove outputChunks as raw data is now streamed.
    • Modified PTY output handling to emit raw_data and file_data events in addition to data events.
    • Ensured emitPendingLines is called on scroll events to prevent loss of lines that fall off the scrollback buffer.
    • Refactored PTY exit handling to wait for all pending writes to complete before finalizing, preventing premature cleanup.
    • Disposed of headlessTerminal on PTY exit to free up resources.
    • Adjusted startLine calculation for serializeTerminalToObject to use headlessTerminal.rows + 1000 as a default if maxSerializedLines is not set, providing a larger buffer for serialization.
  • packages/core/src/services/toolOutputMaskingService.test.ts
    • Added a test case to verify that the masking service correctly uses an existing outputFile path from a tool response when masking large content, rather than creating a new temporary file.
  • packages/core/src/services/toolOutputMaskingService.ts
    • Modified maskToolOutput to check if the tool's original response already contains an outputFile path.
    • If an outputFile is present, the service now uses that path for the masked output reference, avoiding redundant file creation and ensuring consistency.
  • packages/core/src/tools/shell.test.ts
    • Added getTruncateToolOutputThreshold to the mock configuration for ShellTool tests.
  • packages/core/src/tools/shell.ts
    • Imported fs in addition to fsPromises.
    • Added outputFileName, outputFilePath, outputStream, and fullOutputReturned variables to ShellToolInvocation to manage file streaming.
    • Implemented writing file_data events to the outputStream and tracking totalBytesWritten.
    • Ensured outputStream.end() is awaited before returning the result, preventing race conditions.
    • Added fullOutputFilePath to the ToolResult interface, populated if the output exceeds the truncation threshold.
    • Included cleanup logic to destroy the outputStream and unlink the temporary output file if it was not moved to a persistent location.
  • packages/core/src/tools/tools.ts
    • Added an optional fullOutputFilePath property to the ToolResult interface, indicating a path to the full, non-truncated output.
  • packages/core/src/utils/fileUtils.ts
    • Added a new asynchronous function moveToolOutputToFile to handle moving a source file to a designated temporary output directory, including cross-filesystem rename/copy logic.
    • Updated formatTruncatedToolOutput to include the outputFile path in the truncated message.
  • packages/core/src/utils/generateContentResponseUtilities.ts
    • Modified createFunctionResponsePart to accept an optional outputFile parameter and include it in the function response.
    • Updated convertToFunctionResponse to pass the outputFile to createFunctionResponsePart when constructing the function response.
Activity
  • jacob314 initiated the pull request with a focus on fixing CLI issues related to efficient shell output streaming.
  • The author made several commits, including one to address a critical race condition by awaiting outputStream.end() and another to stream processed xterm output to files, removing spurious escape codes.
  • The pull request description includes a detailed pre-merge checklist, indicating the author's readiness for review and thoroughness in validating changes across various platforms and methods.
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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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.

@google-gemini google-gemini deleted a comment from gemini-cli bot Feb 26, 2026
@github-actions
Copy link

github-actions bot commented Feb 26, 2026

Size Change: +12.6 kB (+0.05%)

Total Size: 25.7 MB

Filename Size Change
./bundle/gemini.js 25.2 MB +12.6 kB (+0.05%)
ℹ️ View Unchanged
Filename Size
./bundle/node_modules/@google/gemini-cli-devtools/dist/client/main.js 221 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/_client-assets.js 227 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/index.js 11.5 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/types.js 132 B
./bundle/sandbox-macos-permissive-open.sb 890 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB
./bundle/sandbox-macos-strict-open.sb 4.82 kB
./bundle/sandbox-macos-strict-proxied.sb 5.02 kB

compressed-size-action

Copy link
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 a more efficient way to handle large shell command outputs by streaming them to a file instead of holding them in memory, which is a great improvement for performance and stability. However, a critical security vulnerability was identified in the ToolExecutor class, which trusts file paths returned by tools without validation. This could allow a malicious tool or MCP server to read or delete arbitrary files on the user's system. Additionally, there is a critical data loss regression in the binary output handling and an inconsistency in how log files are created for different types of shell commands that need to be addressed to ensure robustness and a consistent user experience.

Comment on lines +236 to +262
if (toolResult.fullOutputFilePath) {
const threshold = this.config.getTruncateToolOutputThreshold();
if (
threshold > 0 &&
typeof content === 'string' &&
content.length > threshold
) {
const { outputFile: savedPath } = await moveToolOutputToFile(
toolResult.fullOutputFilePath,
toolName,
callId,
this.config.storage.getProjectTempDir(),
this.config.getSessionId(),
);
outputFile = savedPath;
content = formatTruncatedToolOutput(content, outputFile, threshold);
} else {
// If the content is not truncated, we don't need the temporary file.
try {
await fsPromises.unlink(toolResult.fullOutputFilePath);
} catch (error) {
debugLogger.warn(
`Failed to delete temporary tool output file: ${toolResult.fullOutputFilePath}`,
error,
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The ToolExecutor class trusts the fullOutputFilePath field returned in a ToolResult by any tool (including third-party MCP tools). This path is used in several dangerous file operations without validation:

  1. Arbitrary File Read/Exfiltration: On line 243, moveToolOutputToFile is called with toolResult.fullOutputFilePath. This function renames or copies the file to the project's temporary directory. The new path is then returned to the LLM in the function response (line 290). A malicious tool could return a sensitive path (e.g., /etc/passwd or ~/.ssh/id_rsa), causing the CLI to move it to an allowed directory and expose its contents to the LLM.
  2. Arbitrary File Deletion: On lines 127, 144, and 255, the code calls fsPromises.unlink(toolResult.fullOutputFilePath). A malicious tool could return a path to a critical system file, which would then be deleted by the CLI if the execution is aborted or fails.

Remediation:
Before performing any file operations on toolResult.fullOutputFilePath, verify that the path is located within a safe, designated temporary directory (e.g., os.tmpdir()).

Comment on lines 352 to 357
case 'raw_data':
if (!isBinaryStream) {
outputStream.write(event.chunk);
totalBytesWritten += Buffer.byteLength(event.chunk);
}
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This case handles raw_data events for streaming output to a file. However, raw_data contains unprocessed PTY escape codes, which will result in a log file that is not clean and may be difficult for users to read.

For consistency and better user experience, this should be changed to handle the file_data event instead. The file_data event provides processed, clean text suitable for logging, which is already being correctly used by the run_shell_command tool implementation.

Suggested change
case 'raw_data':
if (!isBinaryStream) {
outputStream.write(event.chunk);
totalBytesWritten += Buffer.byteLength(event.chunk);
}
break;
case 'file_data':
if (!isBinaryStream) {
outputStream.write(event.chunk);
totalBytesWritten += Buffer.byteLength(event.chunk);
}
break;


const onAbort = () => combinedController.abort();

const outputFileName = `gemini_shell_output_${crypto.randomBytes(6).toString('hex')}.log`;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've gotten a bit lost in the code. seems like shellExecutionService.ts has the core clever logic for this but maybe I'm misunderstanding how this all works.

@jacob314 jacob314 requested a review from galz10 February 26, 2026 08:42
@gemini-cli gemini-cli bot added priority/p2 Important but can be addressed in a future release. area/core Issues related to User Interface, OS Support, Core Functionality 🔒 maintainer only ⛔ Do not contribute. Internal roadmap item. labels Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality 🔒 maintainer only ⛔ Do not contribute. Internal roadmap item. priority/p2 Important but can be addressed in a future release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Asking agent to run a CLI command that does a lot of shell "magic" is really slow

1 participant