Skip to content

Commit 818b30b

Browse files
committed
fix: cap and flush slash command stdout
1 parent 059afb2 commit 818b30b

1 file changed

Lines changed: 25 additions & 8 deletions

File tree

src/node/services/slashCommandService.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)