Skip to content

fix (desktop): fix crash on input too big -> pty buffer backpressure#765

Closed
AviPeltz wants to merge 1 commit intomainfrom
test-mason
Closed

fix (desktop): fix crash on input too big -> pty buffer backpressure#765
AviPeltz wants to merge 1 commit intomainfrom
test-mason

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Jan 15, 2026

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes

Summary by CodeRabbit

  • Performance Improvements
    • Optimized terminal input handling to prevent event loop blocking, improving application responsiveness during intensive terminal operations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

A new InputWriter class is introduced to handle non-blocking writes to PTY by buffering and chunking large writes. The TerminalSession integrates this writer, and TerminalManager dispatches all write operations through it instead of directly to the PTY.

Changes

Cohort / File(s) Summary
Input Writer Implementation
apps/desktop/src/main/lib/terminal/input-writer.ts
New class for non-blocking PTY writes. Implements chunking strategy (4KB chunks) with setImmediate between writes, queue management, error logging, and lifecycle disposal.
Terminal Session Integration
apps/desktop/src/main/lib/terminal/types.ts, apps/desktop/src/main/lib/terminal/session.ts
Extended TerminalSession interface with inputWriter property. Modified createSession to instantiate InputWriter and flushSession to dispose of it.
Terminal Manager Updates
apps/desktop/src/main/lib/terminal/manager.ts
Updated write() and refreshPromptsForWorkspace() to dispatch writes via session.inputWriter.write() instead of session.pty.write().

Sequence Diagram

sequenceDiagram
    participant Manager as TerminalManager
    participant InputWriter as InputWriter
    participant Queue as Write Queue
    participant PTY as PTY Process

    Manager->>InputWriter: write(largeData)
    alt Data Size < CHUNK_THRESHOLD
        InputWriter->>PTY: write(data)
    else Data Size >= CHUNK_THRESHOLD
        InputWriter->>Queue: enqueue(data)
        InputWriter->>InputWriter: processQueue()
    end
    
    loop For Each Chunk
        InputWriter->>PTY: write(CHUNK_SIZE)
        InputWriter->>Queue: dequeue & update
        InputWriter->>InputWriter: setImmediate(writeNextChunk)
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Poem

🐰 A writer non-blocking, so clever and keen,
Chunks up the data—a buffering machine!
The PTY won't freeze when the words start to pour,
Our terminal hops faster than e'er before!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete. Only the 'Type of Change' section (Bug fix) is marked; all other sections including Description, Related Issues, Testing, and Additional Notes are empty or contain only template comments. Complete the Description section explaining the crash/buffer backpressure issue, add Related Issues links if applicable, provide Testing details on how the fix was validated, and include relevant context in Additional Notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: introducing non-blocking input handling to prevent crashes from large PTY input causing buffer backpressure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/main/lib/terminal/session.ts (1)

202-204: Use inputWriter for initial commands to match the pattern in manager.ts.

setupDataHandler bypasses InputWriter when writing initial commands directly via session.pty.write(), while manager.ts uses session.inputWriter.write() for all other input to prevent main thread blocking. For consistency and to handle potentially large initial command strings (especially when multiple setup commands are joined with &&), use session.inputWriter.write(initialCommandString) instead.

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/lib/terminal/session.ts`:
- Around line 212-216: The test mock for TerminalSession is missing the required
inputWriter, causing flushSession(session: TerminalSession) to throw when it
calls session.inputWriter.dispose(); update the test's TerminalSession mock used
in the failing test to include an inputWriter object with a dispose() stub
(matching other mocks like dataBatcher.dispose and headless.dispose), or if you
prefer the runtime to be defensive, change flushSession to guard the call (e.g.,
use optional chaining when invoking session.inputWriter.dispose()), but the
preferred fix is to add an inputWriter mock so flushSession can call dispose on
a defined object.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between facd7a5 and 182a539.

📒 Files selected for processing (4)
  • apps/desktop/src/main/lib/terminal/input-writer.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/input-writer.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/input-writer.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/input-writer.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/input-writer.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
🧬 Code graph analysis (2)
apps/desktop/src/main/lib/terminal/session.ts (1)
apps/desktop/src/main/lib/terminal/input-writer.ts (1)
  • InputWriter (28-133)
apps/desktop/src/main/lib/terminal/types.ts (1)
apps/desktop/src/main/lib/terminal/input-writer.ts (1)
  • InputWriter (28-133)
🪛 GitHub Check: Test
apps/desktop/src/main/lib/terminal/session.ts

[failure] 213-213: TypeError: undefined is not an object (evaluating 'session.inputWriter.dispose')

  at flushSession (/home/runner/work/superset/superset/apps/desktop/src/main/lib/terminal/session.ts:213:10)
  at <anonymous> (/home/runner/work/superset/superset/apps/desktop/src/main/lib/terminal/session.test.ts:109:4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Typecheck
  • GitHub Check: Build
🔇 Additional comments (7)
apps/desktop/src/main/lib/terminal/input-writer.ts (3)

1-36: LGTM on class structure and constants.

The constants are well-documented, and the class setup with proper state management looks correct. The type-only import of node-pty is appropriate.


44-62: LGTM on write method logic.

The early return for disposed state and the branching between direct and queued writes is well-structured. Error handling with contextual logging follows the coding guidelines.


128-132: LGTM on dispose implementation.

The disposal sequence correctly sets the disposed flag first, ensuring any in-flight setImmediate callbacks will exit early when they check isDisposed.

apps/desktop/src/main/lib/terminal/types.ts (1)

5-5: LGTM on type definitions.

The type-only import and interface extension correctly define the new inputWriter property for TerminalSession.

Also applies to: 20-20

apps/desktop/src/main/lib/terminal/manager.ts (2)

167-170: LGTM on write path update.

The switch to inputWriter.write() correctly implements the non-blocking write mechanism. The error handling and session validation remain intact.


391-391: LGTM on prompt refresh update.

Using inputWriter.write("\n") is consistent with the new write path. The single newline character will take the fast direct-write path.

apps/desktop/src/main/lib/terminal/session.ts (1)

134-149: LGTM on InputWriter instantiation and session integration.

The InputWriter is correctly instantiated with the PTY process and properly included in the returned session object.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +90 to +92
// Write one chunk
const chunk = data.slice(0, CHUNK_SIZE);
const remaining = data.slice(CHUNK_SIZE);
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.

⚠️ Potential issue | 🟠 Major

Unicode characters may be split mid-sequence when chunking.

String.slice() operates on UTF-16 code units, not Unicode grapheme clusters. When pasting Unicode text (which this PR specifically aims to support), slicing at arbitrary positions can split surrogate pairs or multi-byte sequences, potentially corrupting characters like emojis or CJK text.

Consider aligning chunk boundaries to avoid splitting characters:

Proposed fix using a boundary-aware slice
-		// Write one chunk
-		const chunk = data.slice(0, CHUNK_SIZE);
-		const remaining = data.slice(CHUNK_SIZE);
+		// Write one chunk, ensuring we don't split surrogate pairs
+		let boundary = Math.min(CHUNK_SIZE, data.length);
+		// If boundary falls between surrogate pair, adjust
+		if (boundary < data.length) {
+			const charCode = data.charCodeAt(boundary - 1);
+			// High surrogate: 0xD800-0xDBFF
+			if (charCode >= 0xD800 && charCode <= 0xDBFF) {
+				boundary--; // Don't split the pair
+			}
+		}
+		const chunk = data.slice(0, boundary);
+		const remaining = data.slice(boundary);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Write one chunk
const chunk = data.slice(0, CHUNK_SIZE);
const remaining = data.slice(CHUNK_SIZE);
// Write one chunk, ensuring we don't split surrogate pairs
let boundary = Math.min(CHUNK_SIZE, data.length);
// If boundary falls between surrogate pair, adjust
if (boundary < data.length) {
const charCode = data.charCodeAt(boundary - 1);
// High surrogate: 0xD800-0xDBFF
if (charCode >= 0xD800 && charCode <= 0xDBFF) {
boundary--; // Don't split the pair
}
}
const chunk = data.slice(0, boundary);
const remaining = data.slice(boundary);

Comment on lines 212 to 216
export function flushSession(session: TerminalSession): void {
session.inputWriter.dispose();
session.dataBatcher.dispose();
session.headless.dispose();
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the session.ts file and examine the relevant code
find . -name "session.ts" -path "*apps/desktop*" | head -5

Repository: superset-sh/superset

Length of output: 111


🏁 Script executed:

# Examine the session.ts file to understand the flushSession function and TerminalSession type
cat -n ./apps/desktop/src/main/lib/terminal/session.ts | sed -n '1,250p'

Repository: superset-sh/superset

Length of output: 6719


🏁 Script executed:

# Find and examine the types.ts file for TerminalSession type definition
fd "types.ts" -path "*apps/desktop/src/main/lib/terminal*" --type f

Repository: superset-sh/superset

Length of output: 232


🏁 Script executed:

# Find and examine the types.ts file for TerminalSession type definition
fd "types.ts" -path "*apps/desktop/src/main/lib/terminal*" -type f

Repository: superset-sh/superset

Length of output: 232


🏁 Script executed:

# Find and examine the types.ts file for TerminalSession type definition
find ./apps/desktop/src/main/lib/terminal -name "types.ts"

Repository: superset-sh/superset

Length of output: 109


🏁 Script executed:

# Examine the types.ts file to understand TerminalSession interface
cat -n ./apps/desktop/src/main/lib/terminal/types.ts

Repository: superset-sh/superset

Length of output: 1849


🏁 Script executed:

# Find test files related to session
find ./apps/desktop -name "*.test.ts" -o -name "*.test.tsx" | xargs grep -l "flushSession\|session" 2>/dev/null | head -10

Repository: superset-sh/superset

Length of output: 169


🏁 Script executed:

# Examine the session.test.ts file
cat -n ./apps/desktop/src/main/lib/terminal/session.test.ts

Repository: superset-sh/superset

Length of output: 3677


🏁 Script executed:

# Check if there's a git history or any recent changes to understand if this is a new issue
git log --oneline -20 ./apps/desktop/src/main/lib/terminal/session.ts

Repository: superset-sh/superset

Length of output: 98


🏁 Script executed:

# Check the git diff to see what changes were made in that commit
git show 182a539 --stat

Repository: superset-sh/superset

Length of output: 50376


🏁 Script executed:

# Check the diff specifically for session.ts
git show 182a539 -- apps/desktop/src/main/lib/terminal/session.ts

Repository: superset-sh/superset

Length of output: 5816


🏁 Script executed:

# Check if inputWriter is ever made optional or if there are other callers of flushSession
rg "flushSession" apps/desktop/src/main/lib/terminal/

Repository: superset-sh/superset

Length of output: 537


🏁 Script executed:

# Check how flushSession is used in manager.ts
rg -A 3 -B 3 "flushSession\(session" apps/desktop/src/main/lib/terminal/manager.ts

Repository: superset-sh/superset

Length of output: 333


🏁 Script executed:

# Check if there are any other test files for terminal/session functionality
find apps/desktop -name "*session*test*" -o -name "*test*session*" | grep -i terminal

Repository: superset-sh/superset

Length of output: 114


🏁 Script executed:

# Check manager.test.ts for flushSession usage
rg -A 10 -B 5 "flushSession" apps/desktop/src/main/lib/terminal/manager.test.ts | head -50

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Let's look at the full test to understand what it's actually testing
sed -n '87,115p' apps/desktop/src/main/lib/terminal/session.test.ts

Repository: superset-sh/superset

Length of output: 662


The test mock is incomplete and will cause flushSession to fail.

The test creates a partial mock of TerminalSession that omits the inputWriter property (line 104-107), but flushSession unconditionally calls session.inputWriter.dispose() on line 213. This will throw "Cannot read property 'dispose' of undefined" at runtime.

Fix the mock to include inputWriter:

Add inputWriter to test mock
 const mockSession = {
     dataBatcher: mockDataBatcher,
     headless: mockHeadless,
+    inputWriter: { dispose: () => { /* track if called */ } },
 } as unknown as TerminalSession;

Alternatively, use optional chaining in flushSession to be defensive, though the type system defines inputWriter as required.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function flushSession(session: TerminalSession): void {
session.inputWriter.dispose();
session.dataBatcher.dispose();
session.headless.dispose();
}
export function flushSession(session: TerminalSession): void {
session.inputWriter?.dispose();
session.dataBatcher?.dispose();
session.headless?.dispose();
}
🧰 Tools
🪛 GitHub Check: Test

[failure] 213-213: TypeError: undefined is not an object (evaluating 'session.inputWriter.dispose')

  at flushSession (/home/runner/work/superset/superset/apps/desktop/src/main/lib/terminal/session.ts:213:10)
  at <anonymous> (/home/runner/work/superset/superset/apps/desktop/src/main/lib/terminal/session.test.ts:109:4)
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/terminal/session.ts` around lines 212 - 216, The
test mock for TerminalSession is missing the required inputWriter, causing
flushSession(session: TerminalSession) to throw when it calls
session.inputWriter.dispose(); update the test's TerminalSession mock used in
the failing test to include an inputWriter object with a dispose() stub
(matching other mocks like dataBatcher.dispose and headless.dispose), or if you
prefer the runtime to be defensive, change flushSession to guard the call (e.g.,
use optional chaining when invoking session.inputWriter.dispose()), but the
preferred fix is to add an inputWriter mock so flushSession can call dispose on
a defined object.

@AviPeltz AviPeltz closed this Jan 15, 2026
@AviPeltz
Copy link
Copy Markdown
Collaborator Author

fixed in persistence pr

@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

@Kitenite Kitenite deleted the test-mason branch January 16, 2026 18:59
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.

1 participant