@@ -122,10 +122,31 @@ export class SlashCommandService extends EventEmitter {
122122 } satisfies WorkspaceInitEvent & { workspaceId : string } ) ;
123123
124124 // Accumulate raw stdout chunks for return value (preserves empty lines)
125+ //
126+ // Note: We intentionally cap stdout to avoid holding arbitrarily large command output in memory.
125127 const stdoutChunks : Uint8Array [ ] = [ ] ;
126128 let stdoutByteLength = 0 ;
129+ let stdoutTruncated = false ;
127130 const MAX_STDOUT_BYTES = 1024 * 1024 ; // 1MB limit
128131
132+ const appendStdoutChunk = ( chunk : Uint8Array ) => {
133+ if ( stdoutTruncated ) return ;
134+
135+ const remaining = MAX_STDOUT_BYTES - stdoutByteLength ;
136+ if ( remaining <= 0 ) {
137+ stdoutTruncated = true ;
138+ return ;
139+ }
140+
141+ const slice = chunk . length <= remaining ? chunk : chunk . subarray ( 0 , remaining ) ;
142+ stdoutChunks . push ( slice ) ;
143+ stdoutByteLength += slice . length ;
144+
145+ if ( chunk . length > remaining ) {
146+ stdoutTruncated = true ;
147+ }
148+ } ;
149+
129150 // LineBuffer for streaming display (may drop empty lines, that's OK for live display)
130151 const stdoutBuffer = new LineBuffer ( ( line ) => {
131152 // Emit for live display
@@ -178,13 +199,11 @@ export class SlashCommandService extends EventEmitter {
178199 const { done, value } = await reader . read ( ) ;
179200 if ( done ) break ;
180201 // Accumulate raw bytes for final output (preserves empty lines)
181- if ( stdoutByteLength < MAX_STDOUT_BYTES ) {
182- stdoutChunks . push ( value ) ;
183- stdoutByteLength += value . length ;
184- }
202+ appendStdoutChunk ( value ) ;
185203 // Stream decoded text for live display (may drop empty lines)
186204 stdoutBuffer . append ( decoder . decode ( value , { stream : true } ) ) ;
187205 }
206+ stdoutBuffer . append ( decoder . decode ( ) ) ;
188207 stdoutBuffer . flush ( ) ;
189208 } finally {
190209 reader . releaseLock ( ) ;
@@ -200,6 +219,7 @@ export class SlashCommandService extends EventEmitter {
200219 if ( done ) break ;
201220 stderrBuffer . append ( decoder . decode ( value , { stream : true } ) ) ;
202221 }
222+ stderrBuffer . append ( decoder . decode ( ) ) ;
203223 stderrBuffer . flush ( ) ;
204224 } finally {
205225 reader . releaseLock ( ) ;
@@ -217,17 +237,14 @@ export class SlashCommandService extends EventEmitter {
217237 workspaceId,
218238 exitCode,
219239 timestamp : endTime ,
220- ...( stdoutByteLength >= MAX_STDOUT_BYTES ? { truncated : true } : { } ) ,
221240 } satisfies WorkspaceInitEvent & { workspaceId : string } ) ;
222241
223242 log . debug (
224243 `Slash command /${ name } completed (exit ${ exitCode } , duration ${ endTime - startTime } ms)`
225244 ) ;
226245
227246 // Combine chunks and decode to string (preserves empty lines)
228- const combinedStdout = new Uint8Array (
229- stdoutChunks . reduce ( ( acc , chunk ) => acc + chunk . length , 0 )
230- ) ;
247+ const combinedStdout = new Uint8Array ( stdoutByteLength ) ;
231248 let offset = 0 ;
232249 for ( const chunk of stdoutChunks ) {
233250 combinedStdout . set ( chunk , offset ) ;
0 commit comments