Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/core/src/services/shellExecutionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,42 @@ describe('ShellExecutionService', () => {
expect(result.output).toBe('');
expect(onOutputEventMock).not.toHaveBeenCalled();
});

it('should truncate PTY output using a sliding window and show a warning', async () => {
const MAX_SIZE = 16 * 1024 * 1024;
const chunk1 = 'a'.repeat(MAX_SIZE / 2 - 5);
const chunk2 = 'b'.repeat(MAX_SIZE / 2 - 5);
const chunk3 = 'c'.repeat(20);

const { result } = await simulateExecution(
'large-output',
async (pty) => {
pty.onData.mock.calls[0][0](chunk1);
await new Promise((resolve) => setImmediate(resolve));
pty.onData.mock.calls[0][0](chunk2);
await new Promise((resolve) => setImmediate(resolve));
pty.onData.mock.calls[0][0](chunk3);
await new Promise((resolve) => setImmediate(resolve));
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
},
);

const truncationMessage =
'[LLXPRT_CODE_WARNING: Output truncated. The buffer is limited to 16MB.]';
expect(result.output).toContain(truncationMessage);

const outputWithoutMessage = result.output
.substring(0, result.output.indexOf(truncationMessage))
.trimEnd();

expect(outputWithoutMessage.length).toBe(MAX_SIZE);

const expectedStart = (chunk1 + chunk2 + chunk3).slice(-MAX_SIZE);
expect(
outputWithoutMessage.startsWith(expectedStart.substring(0, 10)),
).toBe(true);
expect(outputWithoutMessage.endsWith('c'.repeat(20))).toBe(true);
}, 20000);
});

describe('Failed Execution', () => {
Expand Down
46 changes: 32 additions & 14 deletions packages/core/src/services/shellExecutionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,8 @@ function stripAnsiIfPresent(value: string): string {
: value;
}

// @ts-expect-error getFullText is not a public API.
const getFullText = (terminal: Terminal) => {
const buffer = terminal.buffer.active;
const lines: string[] = [];
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
lines.push(line ? line.translateToString(true) : '');
}
return lines.join('\n').trim();
};
// Note: getFullText was removed as the PTY path now uses truncatedOutput
// for bounded memory instead of extracting the full xterm terminal buffer.

/** A structured result from a shell command execution. */
export interface ShellExecutionResult {
Expand Down Expand Up @@ -429,6 +421,10 @@ export class ShellExecutionService {
let sniffBuffer = Buffer.alloc(0);
let totalBytesReceived = 0;

// Truncation tracking for PTY output (mirrors child_process path)
let truncatedOutput = '';
let outputTruncated = false;

const handleOutput = (data: Buffer) => {
processingChain = processingChain.then(
() =>
Expand Down Expand Up @@ -464,10 +460,23 @@ export class ShellExecutionService {

if (isStreamingRawContent) {
const decodedChunk = decoder.decode(data, { stream: true });
const strippedChunk = stripAnsiIfPresent(decodedChunk);

// Apply truncation to maintain bounded memory
const { newBuffer, truncated } = this.appendAndTruncate(
truncatedOutput,
strippedChunk,
MAX_CHILD_PROCESS_BUFFER_SIZE,
);
truncatedOutput = newBuffer;
if (truncated) {
outputTruncated = true;
}

headlessTerminal.write(decodedChunk, () => {
onOutputEvent({
type: 'data',
chunk: stripAnsiIfPresent(decodedChunk),
chunk: strippedChunk,
});
resolve();
});
Expand Down Expand Up @@ -495,11 +504,20 @@ export class ShellExecutionService {
const finalize = () => {
const finalBuffer = Buffer.concat(outputChunks);

const fullOutput = getFullText(headlessTerminal);
// Use our truncated output instead of the unbounded terminal buffer
let finalOutput = truncatedOutput;
if (outputTruncated) {
const truncationMessage = `
[LLXPRT_CODE_WARNING: Output truncated. The buffer is limited to ${
MAX_CHILD_PROCESS_BUFFER_SIZE / (1024 * 1024)
}MB.]`;
finalOutput += truncationMessage;
}

resolve({
rawOutput: finalBuffer,
output: fullOutput,
stdout: fullOutput, // For PTY, stdout and stderr are combined
output: finalOutput.trim(),
stdout: truncatedOutput, // For PTY, stdout and stderr are combined
stderr: '', // PTY combines output streams
exitCode,
signal: signal ?? null,
Expand Down
Loading