mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
perf: add BufferAccumulator to reduce GC pressure in hot paths
Replace string concatenation (+=) with array-based BufferAccumulator in terminal buffer handling. This reduces garbage collection pressure during high-throughput terminal streaming. Changes: - Add BufferAccumulator class with auto-trim on max size - Convert _terminalBuffer and _textOutput to BufferAccumulator in session.ts - Convert terminalBuffer to BufferAccumulator in respawn-controller.ts - Remove manual trim logic (now handled by BufferAccumulator) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+61
-14
@@ -64,6 +64,57 @@ const WHITESPACE_PATTERN = /\s+/g;
|
|||||||
*/
|
*/
|
||||||
const READY_INDICATOR = '↵ send';
|
const READY_INDICATOR = '↵ send';
|
||||||
|
|
||||||
|
// ========== Buffer Accumulator ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-performance buffer accumulator using array-based collection.
|
||||||
|
* Reduces GC pressure by avoiding repeated string concatenation.
|
||||||
|
*/
|
||||||
|
class BufferAccumulator {
|
||||||
|
private chunks: string[] = [];
|
||||||
|
private totalLength: number = 0;
|
||||||
|
private readonly maxSize: number;
|
||||||
|
private readonly trimSize: number;
|
||||||
|
|
||||||
|
constructor(maxSize: number, trimSize: number) {
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
this.trimSize = trimSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(data: string): void {
|
||||||
|
if (!data) return;
|
||||||
|
this.chunks.push(data);
|
||||||
|
this.totalLength += data.length;
|
||||||
|
if (this.totalLength > this.maxSize) {
|
||||||
|
this.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): string {
|
||||||
|
if (this.chunks.length === 0) return '';
|
||||||
|
if (this.chunks.length === 1) return this.chunks[0];
|
||||||
|
const result = this.chunks.join('');
|
||||||
|
this.chunks = [result];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.totalLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.chunks = [];
|
||||||
|
this.totalLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private trim(): void {
|
||||||
|
const full = this.chunks.join('');
|
||||||
|
const trimmed = full.slice(-this.trimSize);
|
||||||
|
this.chunks = [trimmed];
|
||||||
|
this.totalLength = trimmed.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Type Definitions ==========
|
// ========== Type Definitions ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,8 +315,8 @@ export class RespawnController extends EventEmitter {
|
|||||||
/** Timestamp of last terminal activity */
|
/** Timestamp of last terminal activity */
|
||||||
private lastActivityTime: number = 0;
|
private lastActivityTime: number = 0;
|
||||||
|
|
||||||
/** Buffer for recent terminal output */
|
/** Buffer for recent terminal output (uses BufferAccumulator to reduce GC pressure) */
|
||||||
private terminalBuffer: string = '';
|
private terminalBuffer = new BufferAccumulator(MAX_RESPAWN_BUFFER_SIZE, RESPAWN_BUFFER_TRIM_SIZE);
|
||||||
|
|
||||||
/** Whether a prompt indicator was detected */
|
/** Whether a prompt indicator was detected */
|
||||||
private promptDetected: boolean = false;
|
private promptDetected: boolean = false;
|
||||||
@@ -466,12 +517,8 @@ export class RespawnController extends EventEmitter {
|
|||||||
* @param data - Raw terminal output data
|
* @param data - Raw terminal output data
|
||||||
*/
|
*/
|
||||||
private handleTerminalData(data: string): void {
|
private handleTerminalData(data: string): void {
|
||||||
this.terminalBuffer += data;
|
// BufferAccumulator handles auto-trimming when max size exceeded
|
||||||
|
this.terminalBuffer.append(data);
|
||||||
// Keep buffer manageable (max 1MB, trim to 512KB)
|
|
||||||
if (this.terminalBuffer.length > MAX_RESPAWN_BUFFER_SIZE) {
|
|
||||||
this.terminalBuffer = this.terminalBuffer.slice(-RESPAWN_BUFFER_TRIM_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for the definitive "ready for input" indicator
|
// Check for the definitive "ready for input" indicator
|
||||||
const isReady = data.includes(READY_INDICATOR);
|
const isReady = data.includes(READY_INDICATOR);
|
||||||
@@ -606,7 +653,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private startMonitoringInit(): void {
|
private startMonitoringInit(): void {
|
||||||
this.setState('monitoring_init');
|
this.setState('monitoring_init');
|
||||||
this.terminalBuffer = '';
|
this.terminalBuffer.clear();
|
||||||
this.workingDetected = false;
|
this.workingDetected = false;
|
||||||
this.log('Monitoring if /init triggered work...');
|
this.log('Monitoring if /init triggered work...');
|
||||||
|
|
||||||
@@ -641,7 +688,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private sendKickstart(): void {
|
private sendKickstart(): void {
|
||||||
this.setState('sending_kickstart');
|
this.setState('sending_kickstart');
|
||||||
this.terminalBuffer = '';
|
this.terminalBuffer.clear();
|
||||||
|
|
||||||
this.stepTimer = setTimeout(() => {
|
this.stepTimer = setTimeout(() => {
|
||||||
const prompt = this.config.kickstartPrompt!;
|
const prompt = this.config.kickstartPrompt!;
|
||||||
@@ -723,7 +770,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private sendUpdateDocs(): void {
|
private sendUpdateDocs(): void {
|
||||||
this.setState('sending_update');
|
this.setState('sending_update');
|
||||||
this.terminalBuffer = ''; // Clear buffer for fresh detection
|
this.terminalBuffer.clear(); // Clear buffer for fresh detection
|
||||||
|
|
||||||
this.stepTimer = setTimeout(() => {
|
this.stepTimer = setTimeout(() => {
|
||||||
const input = this.config.updatePrompt + '\r'; // \r triggers Enter in Ink/Claude CLI
|
const input = this.config.updatePrompt + '\r'; // \r triggers Enter in Ink/Claude CLI
|
||||||
@@ -742,7 +789,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private sendClear(): void {
|
private sendClear(): void {
|
||||||
this.setState('sending_clear');
|
this.setState('sending_clear');
|
||||||
this.terminalBuffer = '';
|
this.terminalBuffer.clear();
|
||||||
|
|
||||||
this.stepTimer = setTimeout(() => {
|
this.stepTimer = setTimeout(() => {
|
||||||
this.log('Sending /clear');
|
this.log('Sending /clear');
|
||||||
@@ -759,7 +806,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private sendInit(): void {
|
private sendInit(): void {
|
||||||
this.setState('sending_init');
|
this.setState('sending_init');
|
||||||
this.terminalBuffer = '';
|
this.terminalBuffer.clear();
|
||||||
|
|
||||||
this.stepTimer = setTimeout(() => {
|
this.stepTimer = setTimeout(() => {
|
||||||
this.log('Sending /init');
|
this.log('Sending /init');
|
||||||
@@ -782,7 +829,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
|
|
||||||
// Go back to watching state for next cycle
|
// Go back to watching state for next cycle
|
||||||
this.setState('watching');
|
this.setState('watching');
|
||||||
this.terminalBuffer = '';
|
this.terminalBuffer.clear();
|
||||||
this.promptDetected = false;
|
this.promptDetected = false;
|
||||||
this.workingDetected = false;
|
this.workingDetected = false;
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-50
@@ -60,6 +60,77 @@ const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
|||||||
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||||
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Buffer Accumulator (reduces GC pressure from string concatenation)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-performance buffer accumulator using array-based collection.
|
||||||
|
*
|
||||||
|
* Reduces GC pressure by avoiding repeated string concatenation (`+=`).
|
||||||
|
* Instead, chunks are pushed to an array and joined only when needed.
|
||||||
|
* Automatically trims when size limits are exceeded.
|
||||||
|
*/
|
||||||
|
class BufferAccumulator {
|
||||||
|
private chunks: string[] = [];
|
||||||
|
private totalLength: number = 0;
|
||||||
|
private readonly maxSize: number;
|
||||||
|
private readonly trimSize: number;
|
||||||
|
|
||||||
|
constructor(maxSize: number, trimSize: number) {
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
this.trimSize = trimSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Append data to the buffer */
|
||||||
|
append(data: string): void {
|
||||||
|
if (!data) return;
|
||||||
|
this.chunks.push(data);
|
||||||
|
this.totalLength += data.length;
|
||||||
|
|
||||||
|
// Trim if exceeded max size
|
||||||
|
if (this.totalLength > this.maxSize) {
|
||||||
|
this.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the full buffer content (joins all chunks) */
|
||||||
|
get value(): string {
|
||||||
|
if (this.chunks.length === 0) return '';
|
||||||
|
if (this.chunks.length === 1) return this.chunks[0];
|
||||||
|
|
||||||
|
// Consolidate chunks on access
|
||||||
|
const result = this.chunks.join('');
|
||||||
|
this.chunks = [result];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get current buffer length without joining */
|
||||||
|
get length(): number {
|
||||||
|
return this.totalLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear the buffer */
|
||||||
|
clear(): void {
|
||||||
|
this.chunks = [];
|
||||||
|
this.totalLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set buffer to a specific value */
|
||||||
|
set(value: string): void {
|
||||||
|
this.chunks = value ? [value] : [];
|
||||||
|
this.totalLength = value?.length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Trim buffer to keep only the most recent data */
|
||||||
|
private trim(): void {
|
||||||
|
const full = this.chunks.join('');
|
||||||
|
const trimmed = full.slice(-this.trimSize);
|
||||||
|
this.chunks = [trimmed];
|
||||||
|
this.totalLength = trimmed.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a JSON message from Claude CLI's stream-json output format.
|
* Represents a JSON message from Claude CLI's stream-json output format.
|
||||||
* Messages are newline-delimited JSON objects parsed from PTY output.
|
* Messages are newline-delimited JSON objects parsed from PTY output.
|
||||||
@@ -177,9 +248,10 @@ export class Session extends EventEmitter {
|
|||||||
private _pid: number | null = null;
|
private _pid: number | null = null;
|
||||||
private _status: SessionStatus = 'idle';
|
private _status: SessionStatus = 'idle';
|
||||||
private _currentTaskId: string | null = null;
|
private _currentTaskId: string | null = null;
|
||||||
private _terminalBuffer: string = ''; // Raw terminal output
|
// Use BufferAccumulator for hot-path buffers to reduce GC pressure
|
||||||
|
private _terminalBuffer = new BufferAccumulator(MAX_TERMINAL_BUFFER_SIZE, TERMINAL_BUFFER_TRIM_SIZE);
|
||||||
private _outputBuffer: string = '';
|
private _outputBuffer: string = '';
|
||||||
private _textOutput: string = '';
|
private _textOutput = new BufferAccumulator(MAX_TEXT_OUTPUT_SIZE, TEXT_OUTPUT_TRIM_SIZE);
|
||||||
private _errorBuffer: string = '';
|
private _errorBuffer: string = '';
|
||||||
private _lastActivityAt: number;
|
private _lastActivityAt: number;
|
||||||
private _claudeSessionId: string | null = null;
|
private _claudeSessionId: string | null = null;
|
||||||
@@ -261,7 +333,7 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get terminalBuffer(): string {
|
get terminalBuffer(): string {
|
||||||
return this._terminalBuffer;
|
return this._terminalBuffer.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get outputBuffer(): string {
|
get outputBuffer(): string {
|
||||||
@@ -269,7 +341,7 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get textOutput(): string {
|
get textOutput(): string {
|
||||||
return this._textOutput;
|
return this._textOutput.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorBuffer(): string {
|
get errorBuffer(): string {
|
||||||
@@ -422,8 +494,8 @@ export class Session extends EventEmitter {
|
|||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
claudeSessionId: this._claudeSessionId,
|
claudeSessionId: this._claudeSessionId,
|
||||||
totalCost: this._totalCost,
|
totalCost: this._totalCost,
|
||||||
textOutput: this._textOutput,
|
textOutput: this._textOutput.value,
|
||||||
terminalBuffer: this._terminalBuffer,
|
terminalBuffer: this._terminalBuffer.value,
|
||||||
messageCount: this._messages.length,
|
messageCount: this._messages.length,
|
||||||
isWorking: this._isWorking,
|
isWorking: this._isWorking,
|
||||||
lastPromptTime: this._lastPromptTime,
|
lastPromptTime: this._lastPromptTime,
|
||||||
@@ -479,9 +551,9 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._status = 'busy';
|
this._status = 'busy';
|
||||||
this._terminalBuffer = '';
|
this._terminalBuffer.clear();
|
||||||
this._outputBuffer = '';
|
this._outputBuffer = '';
|
||||||
this._textOutput = '';
|
this._textOutput.clear();
|
||||||
this._errorBuffer = '';
|
this._errorBuffer = '';
|
||||||
this._messages = [];
|
this._messages = [];
|
||||||
this._lineBuffer = '';
|
this._lineBuffer = '';
|
||||||
@@ -521,13 +593,15 @@ export class Session extends EventEmitter {
|
|||||||
if (!isRestoredSession) {
|
if (!isRestoredSession) {
|
||||||
const checkForPrompt = setInterval(() => {
|
const checkForPrompt = setInterval(() => {
|
||||||
// Wait for the prompt character (❯) which means Claude is fully initialized
|
// Wait for the prompt character (❯) which means Claude is fully initialized
|
||||||
if (this._terminalBuffer.includes('❯') || this._terminalBuffer.includes('\u276f')) {
|
const bufferValue = this._terminalBuffer.value;
|
||||||
|
if (bufferValue.includes('❯') || bufferValue.includes('\u276f')) {
|
||||||
clearInterval(checkForPrompt);
|
clearInterval(checkForPrompt);
|
||||||
// Clean the buffer - remove screen init junk before actual content
|
// Clean the buffer - remove screen init junk before actual content
|
||||||
// Strip: cursor movement (\x1b[nA/B/C/D), positioning (\x1b[n;nH),
|
// Strip: cursor movement (\x1b[nA/B/C/D), positioning (\x1b[n;nH),
|
||||||
// clear screen (\x1b[2J), scroll region (\x1b[n;nr), and whitespace
|
// clear screen (\x1b[2J), scroll region (\x1b[n;nr), and whitespace
|
||||||
this._terminalBuffer = this._terminalBuffer
|
this._terminalBuffer.set(
|
||||||
.replace(/^(\x1b\[\??[\d;]*[A-Za-z]|[\s\r\n])+/, '');
|
bufferValue.replace(/^(\x1b\[\??[\d;]*[A-Za-z]|[\s\r\n])+/, '')
|
||||||
|
);
|
||||||
// Signal client to refresh
|
// Signal client to refresh
|
||||||
this.emit('clearTerminal');
|
this.emit('clearTerminal');
|
||||||
}
|
}
|
||||||
@@ -571,14 +645,10 @@ export class Session extends EventEmitter {
|
|||||||
.replace(/\x0c/g, ''); // Remove Ctrl+L
|
.replace(/\x0c/g, ''); // Remove Ctrl+L
|
||||||
if (!data) return; // Skip if only filtered sequences
|
if (!data) return; // Skip if only filtered sequences
|
||||||
|
|
||||||
this._terminalBuffer += data;
|
// BufferAccumulator handles auto-trimming when max size exceeded
|
||||||
|
this._terminalBuffer.append(data);
|
||||||
this._lastActivityAt = Date.now();
|
this._lastActivityAt = Date.now();
|
||||||
|
|
||||||
// Trim buffer if it exceeds max size to prevent memory issues
|
|
||||||
if (this._terminalBuffer.length > MAX_TERMINAL_BUFFER_SIZE) {
|
|
||||||
this._terminalBuffer = this._terminalBuffer.slice(-TERMINAL_BUFFER_TRIM_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('terminal', data);
|
this.emit('terminal', data);
|
||||||
this.emit('output', data);
|
this.emit('output', data);
|
||||||
|
|
||||||
@@ -650,9 +720,9 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._status = 'busy';
|
this._status = 'busy';
|
||||||
this._terminalBuffer = '';
|
this._terminalBuffer.clear();
|
||||||
this._outputBuffer = '';
|
this._outputBuffer = '';
|
||||||
this._textOutput = '';
|
this._textOutput.clear();
|
||||||
this._errorBuffer = '';
|
this._errorBuffer = '';
|
||||||
this._messages = [];
|
this._messages = [];
|
||||||
this._lineBuffer = '';
|
this._lineBuffer = '';
|
||||||
@@ -694,7 +764,7 @@ export class Session extends EventEmitter {
|
|||||||
if (!isRestoredSession) {
|
if (!isRestoredSession) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.ptyProcess) {
|
if (this.ptyProcess) {
|
||||||
this._terminalBuffer = '';
|
this._terminalBuffer.clear();
|
||||||
this.ptyProcess.write('clear\n');
|
this.ptyProcess.write('clear\n');
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -730,14 +800,10 @@ export class Session extends EventEmitter {
|
|||||||
const data = rawData.replace(FOCUS_ESCAPE_FILTER, '');
|
const data = rawData.replace(FOCUS_ESCAPE_FILTER, '');
|
||||||
if (!data) return; // Skip if only focus sequences
|
if (!data) return; // Skip if only focus sequences
|
||||||
|
|
||||||
this._terminalBuffer += data;
|
// BufferAccumulator handles auto-trimming when max size exceeded
|
||||||
|
this._terminalBuffer.append(data);
|
||||||
this._lastActivityAt = Date.now();
|
this._lastActivityAt = Date.now();
|
||||||
|
|
||||||
// Trim buffer if it exceeds max size
|
|
||||||
if (this._terminalBuffer.length > MAX_TERMINAL_BUFFER_SIZE) {
|
|
||||||
this._terminalBuffer = this._terminalBuffer.slice(-TERMINAL_BUFFER_TRIM_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('terminal', data);
|
this.emit('terminal', data);
|
||||||
this.emit('output', data);
|
this.emit('output', data);
|
||||||
});
|
});
|
||||||
@@ -789,9 +855,9 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._status = 'busy';
|
this._status = 'busy';
|
||||||
this._terminalBuffer = '';
|
this._terminalBuffer.clear();
|
||||||
this._outputBuffer = '';
|
this._outputBuffer = '';
|
||||||
this._textOutput = '';
|
this._textOutput.clear();
|
||||||
this._errorBuffer = '';
|
this._errorBuffer = '';
|
||||||
this._messages = [];
|
this._messages = [];
|
||||||
this._lineBuffer = '';
|
this._lineBuffer = '';
|
||||||
@@ -832,14 +898,10 @@ export class Session extends EventEmitter {
|
|||||||
const data = rawData.replace(FOCUS_ESCAPE_FILTER, '');
|
const data = rawData.replace(FOCUS_ESCAPE_FILTER, '');
|
||||||
if (!data) return; // Skip if only focus sequences
|
if (!data) return; // Skip if only focus sequences
|
||||||
|
|
||||||
this._terminalBuffer += data;
|
// BufferAccumulator handles auto-trimming when max size exceeded
|
||||||
|
this._terminalBuffer.append(data);
|
||||||
this._lastActivityAt = Date.now();
|
this._lastActivityAt = Date.now();
|
||||||
|
|
||||||
// Trim buffer if it exceeds max size to prevent memory issues
|
|
||||||
if (this._terminalBuffer.length > MAX_TERMINAL_BUFFER_SIZE) {
|
|
||||||
this._terminalBuffer = this._terminalBuffer.slice(-TERMINAL_BUFFER_TRIM_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('terminal', data);
|
this.emit('terminal', data);
|
||||||
this.emit('output', data);
|
this.emit('output', data);
|
||||||
|
|
||||||
@@ -867,12 +929,12 @@ export class Session extends EventEmitter {
|
|||||||
} else if (exitCode !== 0 || (resultMsg && resultMsg.is_error)) {
|
} else if (exitCode !== 0 || (resultMsg && resultMsg.is_error)) {
|
||||||
this._status = 'error';
|
this._status = 'error';
|
||||||
if (this.rejectPromise) {
|
if (this.rejectPromise) {
|
||||||
this.rejectPromise(new Error(this._errorBuffer || this._textOutput || 'Process exited with error'));
|
this.rejectPromise(new Error(this._errorBuffer || this._textOutput.value || 'Process exited with error'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._status = 'idle';
|
this._status = 'idle';
|
||||||
if (this.resolvePromise) {
|
if (this.resolvePromise) {
|
||||||
this.resolvePromise({ result: this._textOutput || this._terminalBuffer, cost: this._totalCost });
|
this.resolvePromise({ result: this._textOutput.value || this._terminalBuffer.value, cost: this._totalCost });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -895,7 +957,7 @@ export class Session extends EventEmitter {
|
|||||||
// Prevent unbounded line buffer growth for very long lines
|
// Prevent unbounded line buffer growth for very long lines
|
||||||
if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) {
|
if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) {
|
||||||
// Force flush the oversized buffer as text output
|
// Force flush the oversized buffer as text output
|
||||||
this._textOutput += this._lineBuffer + '\n';
|
this._textOutput.append(this._lineBuffer + '\n');
|
||||||
this._lineBuffer = '';
|
this._lineBuffer = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,7 +967,7 @@ export class Session extends EventEmitter {
|
|||||||
this._lineBufferFlushTimer = null;
|
this._lineBufferFlushTimer = null;
|
||||||
if (this._lineBuffer.length > 0) {
|
if (this._lineBuffer.length > 0) {
|
||||||
// Flush partial line as text output
|
// Flush partial line as text output
|
||||||
this._textOutput += this._lineBuffer;
|
this._textOutput.append(this._lineBuffer);
|
||||||
this._lineBuffer = '';
|
this._lineBuffer = '';
|
||||||
}
|
}
|
||||||
}, LINE_BUFFER_FLUSH_INTERVAL);
|
}, LINE_BUFFER_FLUSH_INTERVAL);
|
||||||
@@ -946,7 +1008,7 @@ export class Session extends EventEmitter {
|
|||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === 'text' && block.text) {
|
if (block.type === 'text' && block.text) {
|
||||||
this._textOutput += block.text;
|
this._textOutput.append(block.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Track tokens from usage
|
// Track tokens from usage
|
||||||
@@ -965,17 +1027,13 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not JSON, just regular output
|
// Not JSON, just regular output
|
||||||
this._textOutput += line + '\n';
|
this._textOutput.append(line + '\n');
|
||||||
}
|
}
|
||||||
} else if (trimmed) {
|
} else if (trimmed) {
|
||||||
this._textOutput += line + '\n';
|
this._textOutput.append(line + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Note: BufferAccumulator auto-trims when max size exceeded
|
||||||
// Trim text output buffer for long-running sessions
|
|
||||||
if (this._textOutput.length > MAX_TEXT_OUTPUT_SIZE) {
|
|
||||||
this._textOutput = this._textOutput.slice(-TEXT_OUTPUT_TRIM_SIZE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse token count from Claude's status line in interactive mode
|
// Parse token count from Claude's status line in interactive mode
|
||||||
@@ -1274,7 +1332,7 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOutput(): string {
|
getOutput(): string {
|
||||||
return this._textOutput;
|
return this._textOutput.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getError(): string {
|
getError(): string {
|
||||||
@@ -1282,13 +1340,13 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTerminalBuffer(): string {
|
getTerminalBuffer(): string {
|
||||||
return this._terminalBuffer;
|
return this._terminalBuffer.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearBuffers(): void {
|
clearBuffers(): void {
|
||||||
this._terminalBuffer = '';
|
this._terminalBuffer.clear();
|
||||||
this._outputBuffer = '';
|
this._outputBuffer = '';
|
||||||
this._textOutput = '';
|
this._textOutput.clear();
|
||||||
this._errorBuffer = '';
|
this._errorBuffer = '';
|
||||||
this._messages = [];
|
this._messages = [];
|
||||||
this._taskTracker.clear();
|
this._taskTracker.clear();
|
||||||
|
|||||||
Reference in New Issue
Block a user