diff --git a/packages/code/README.md b/packages/code/README.md
index a38523a3..c7cff6e1 100644
--- a/packages/code/README.md
+++ b/packages/code/README.md
@@ -219,6 +219,20 @@ SRT with:
- bounded time and aggregate output, with best-effort process-group termination
on cancellation, timeout, and completion.
+Native command output keeps a prefix and rolling suffix for each stream, so late
+summaries and errors survive truncation. Each stream stores at most
+`maxOutputBytes` of copied raw bytes while the command runs, independent of output
+volume or chunk count. When both streams are noisy they split the existing combined
+response budget equally, with the odd byte reserved for stderr; a quiet stream gives
+its unused allowance to the other. Sandbox violation annotations enter the same
+stderr window before rendering. UTF-8 boundaries and inline
+`[... N bytes omitted ...]` markers count toward the combined byte limit. The count
+reports omitted raw bytes for that stream, including annotation bytes. If a stream's
+allowance cannot fit its marker, only the retained text and the existing `truncated`
+flag are returned. Truncation does not stop execution. Exit codes, timeout/signal
+fields, and cancellation errors keep their existing semantics, and no new request,
+result, or capability keys are introduced.
+
SRT restrictions remain inherited by descendants. Windows additionally uses a
kill-on-close Job Object. Native macOS does not provide an equivalent hard
process-lifetime boundary: a deliberately daemonized descendant can outlive
diff --git a/packages/code/src/native-sandbox.test.ts b/packages/code/src/native-sandbox.test.ts
index 5b79aa1a..b61041a7 100644
--- a/packages/code/src/native-sandbox.test.ts
+++ b/packages/code/src/native-sandbox.test.ts
@@ -33,6 +33,7 @@ import {
} from './native-sandbox.js';
import { restoreScratchTraversal } from './native-scratch.js';
import { WorkspaceToolError } from './workspace.js';
+import { isWorkspaceToolResult } from './protocol.js';
const request = {
protocolVersion: 1 as const,
@@ -1355,14 +1356,104 @@ test('executes in the canonical workspace and bounds aggregate output', async t
operation: 'execute_command',
workspaceId: 'primary',
exitCode: 0,
- stdout: '1234567890',
- stderr: 'ab',
+ stdout: '123890',
+ stderr: 'abchij',
truncated: true,
timedOut: false,
},
);
});
+test('retains real command summaries on both streams under the legacy combined budget', async t => {
+ const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
+ t.after(() => rm(root, { recursive: true, force: true }));
+ const sandbox = new NativeSrtWorkspaceCommandSandbox({
+ workspaceRoot: root,
+ manager: fakeManager().manager,
+ });
+ t.after(() => sandbox.close());
+ const commandRequest = {
+ ...request,
+ maxOutputBytes: 256,
+ command:
+ "printf 'OUT\\n'; printf '%20000d' 0; printf '\\n42 tests passed\\n'; printf 'ERR\\n' >&2; printf '%20000d' 0 >&2; printf '\\nlate stderr summary\\n' >&2",
+ };
+ const result = await sandbox.execute(commandRequest);
+ assert.equal(result.exitCode, 0);
+ assert.equal(result.timedOut, false);
+ assert.equal(result.truncated, true);
+ assert.ok(result.stdout.startsWith('OUT\n'));
+ assert.ok(result.stderr.startsWith('ERR\n'));
+ assert.ok(result.stdout.endsWith('\n42 tests passed\n'));
+ assert.ok(result.stderr.endsWith('\nlate stderr summary\n'));
+ assert.ok(result.stdout.includes('bytes omitted'));
+ assert.ok(result.stderr.includes('bytes omitted'));
+ assert.equal(isWorkspaceToolResult(commandRequest, result), true);
+});
+
+test('sandbox annotations survive full stdout and remain bounded when stderr is noisy', async t => {
+ const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
+ t.after(() => rm(root, { recursive: true, force: true }));
+ const fake = fakeManager();
+ fake.manager.annotateStderrWithSandboxFailures = (_commandId, stderr) =>
+ stderr + '\n\ndenied write\n';
+ const sandbox = new NativeSrtWorkspaceCommandSandbox({
+ workspaceRoot: root,
+ manager: fake.manager,
+ });
+ t.after(() => sandbox.close());
+ for (const stderrCommand of ['', "printf '%20000d' 0 >&2;"]) {
+ const commandRequest = {
+ ...request,
+ maxOutputBytes: 512,
+ command: `printf '%20000d' 0; ${stderrCommand} true`,
+ };
+ const result = await sandbox.execute(commandRequest);
+ assert.ok(
+ result.stderr.endsWith(
+ '\ndenied write\n'
+ )
+ );
+ assert.equal(isWorkspaceToolResult(commandRequest, result), true);
+ assert.equal(result.truncated, true);
+ }
+ fake.manager.annotateStderrWithSandboxFailures = () => {
+ throw new Error('annotation unavailable');
+ };
+ const result = await sandbox.execute({
+ ...request,
+ maxOutputBytes: 128,
+ command: "printf '%20000d' 0; printf 'child failure' >&2",
+ });
+ assert.equal(result.stderr, 'child failure');
+});
+
+test('timeout settlement keeps late diagnostics, signal, and truncation without widening the result', async t => {
+ const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
+ t.after(() => rm(root, { recursive: true, force: true }));
+ const sandbox = new NativeSrtWorkspaceCommandSandbox({
+ workspaceRoot: root,
+ manager: fakeManager().manager,
+ });
+ t.after(() => sandbox.close());
+ const commandRequest = {
+ ...request,
+ maxOutputBytes: 128,
+ timeoutMs: 100,
+ command:
+ "printf 'START\\n'; printf '%20000d' 0; printf '\\nlast progress\\n'; printf 'last failure' >&2; sleep 30",
+ };
+ const result = await sandbox.execute(commandRequest);
+ assert.ok(result.stdout.startsWith('START\n'));
+ assert.ok(result.stdout.endsWith('\nlast progress\n'));
+ assert.equal(result.stderr, 'last failure');
+ assert.equal(result.timedOut, true);
+ assert.equal(result.truncated, true);
+ assert.equal(result.exitCode, null);
+ assert.equal(result.signal, 'SIGKILL');
+ assert.equal(isWorkspaceToolResult(commandRequest, result), true);
+});
+
test('rejects an escaping or unavailable command working directory', async t => {
const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
t.after(() => rm(root, { recursive: true, force: true }));
diff --git a/packages/code/src/native-sandbox.ts b/packages/code/src/native-sandbox.ts
index 49fe6ecf..6cd5ebd7 100644
--- a/packages/code/src/native-sandbox.ts
+++ b/packages/code/src/native-sandbox.ts
@@ -31,6 +31,7 @@ import {
removePrivateStorageAcl,
} from './private-storage.js';
import { WorkspaceToolError } from './workspace.js';
+import { OutputBuffer, renderCommandOutput } from './output.js';
import { restoreScratchTraversal } from './native-scratch.js';
import { writeLinkedWorktreeGitGuard } from './linked-worktree-git-guard.js';
@@ -1127,28 +1128,10 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox
}
let settled = false;
let timedOut = false;
- let outputBytes = 0;
- let truncated = false;
- const stdout: Buffer[] = [];
- const stderr: Buffer[] = [];
- const append = (target: Buffer[], chunk: Buffer): void => {
- const remaining = outputLimit - outputBytes;
- if (remaining <= 0) {
- truncated = true;
- return;
- }
- const accepted = chunk.subarray(0, remaining);
- target.push(accepted);
- outputBytes += accepted.byteLength;
- if (accepted.byteLength !== chunk.byteLength)
- truncated = true;
- };
- child.stdout.on('data', (chunk: Buffer) =>
- append(stdout, chunk),
- );
- child.stderr.on('data', (chunk: Buffer) =>
- append(stderr, chunk),
- );
+ const stdout = new OutputBuffer(outputLimit);
+ const stderr = new OutputBuffer(outputLimit);
+ child.stdout.on('data', (chunk: Buffer) => stdout.append(chunk));
+ child.stderr.on('data', (chunk: Buffer) => stderr.append(chunk));
const abort = (): void => {
if (settled) return;
this.killCommandTree(child);
@@ -1197,29 +1180,17 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox
);
return;
}
- const stdoutValue = boundedUtf8(
- Buffer.concat(stdout),
- outputLimit,
- );
- const stderrBudget = Math.max(
- 0,
- outputLimit - Buffer.byteLength(stdoutValue),
- );
- const rawStderr = Buffer.concat(stderr).toString('utf8');
- let annotatedStderr = rawStderr;
try {
- annotatedStderr =
- this.manager.annotateStderrWithSandboxFailures(
- commandId,
- rawStderr,
+ // SRT appends violations to its input. Capture them in the same bounded stderr window.
+ stderr.append(
+ Buffer.from(
+ this.manager.annotateStderrWithSandboxFailures(commandId, ''),
+ ),
);
} catch {
// Preserve the bounded child error if optional violation annotation fails.
}
- const stderrValue = boundedUtf8(
- Buffer.from(annotatedStderr),
- stderrBudget,
- );
+ const output = renderCommandOutput(stdout, stderr, outputLimit);
resolvePromise({
protocolVersion: BRIDGE_PROTOCOL_VERSION,
operation: 'execute_command',
@@ -1229,11 +1200,7 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox
? null
: this.protocolExitCode(code),
...(childSignal ? { signal: childSignal } : {}),
- stdout: stdoutValue,
- stderr: stderrValue,
- truncated:
- truncated ||
- Buffer.byteLength(annotatedStderr) > stderrBudget,
+ ...output,
timedOut,
});
});
diff --git a/packages/code/src/output.test.ts b/packages/code/src/output.test.ts
new file mode 100644
index 00000000..3af3c360
--- /dev/null
+++ b/packages/code/src/output.test.ts
@@ -0,0 +1,199 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { OutputBuffer, renderCommandOutput } from './output.js';
+
+function collect(
+ content: Buffer,
+ budget: number,
+ chunkSize = content.length || 1
+): OutputBuffer {
+ const output = new OutputBuffer(budget);
+ for (let offset = 0; offset < content.length; offset += chunkSize) {
+ output.append(content.subarray(offset, offset + chunkSize));
+ }
+ return output;
+}
+
+function assertBounded(text: string, budget: number): void {
+ assert.ok(Buffer.byteLength(text) <= budget);
+ assert.equal(Buffer.from(text).toString('utf8'), text);
+}
+
+test('small output stays byte-for-byte unchanged, including split UTF-8 and exact limits', () => {
+ for (const content of ['', '1234567', 'hello\n世界🌍\n']) {
+ const bytes = Buffer.from(content);
+ const budget = Math.max(1, bytes.length);
+ for (const chunkSize of [1, 2, 7, 64]) {
+ assert.deepEqual(collect(bytes, budget, chunkSize).render(budget), {
+ text: content,
+ truncated: false,
+ });
+ }
+ }
+});
+
+test('a rolling suffix keeps the last summary and reports the exact dropped-byte count', () => {
+ const budget = 128;
+ const content = Buffer.from(
+ 'START\n' + 'x'.repeat(10_000) + '\n42 tests passed\n'
+ );
+ for (const chunkSize of [1, 3, 63, 64, 65, 127, 1024, content.length]) {
+ const result = collect(content, budget, chunkSize).render(budget);
+ assert.equal(result.truncated, true);
+ assert.ok(result.text.startsWith('START\n'));
+ assert.ok(result.text.endsWith('\n42 tests passed\n'));
+ const marker = /\n\[\.\.\. (\d+) bytes omitted \.\.\.\]\n/.exec(
+ result.text
+ );
+ assert.ok(marker);
+ assert.equal(
+ Number(marker[1]),
+ content.length -
+ Buffer.byteLength(result.text.replace(marker[0], ''))
+ );
+ assertBounded(result.text, budget);
+ }
+});
+
+test('either quiet stream donates its budget, and stdout cannot starve stderr', () => {
+ const budget = 256;
+ const empty = collect(Buffer.alloc(0), budget);
+ const exact = collect(Buffer.alloc(budget, 'a'), budget, 1);
+ assert.deepEqual(renderCommandOutput(exact, empty, budget), {
+ stdout: 'a'.repeat(budget),
+ stderr: '',
+ truncated: false,
+ });
+ assert.deepEqual(renderCommandOutput(empty, exact, budget), {
+ stdout: '',
+ stderr: 'a'.repeat(budget),
+ truncated: false,
+ });
+ const stdout = collect(
+ Buffer.from('OUT\n' + 'x'.repeat(4096) + '\nstdout summary'),
+ budget
+ );
+ const stderr = collect(
+ Buffer.from('ERR\n' + 'y'.repeat(4096) + '\nstderr summary'),
+ budget
+ );
+ const result = renderCommandOutput(stdout, stderr, budget);
+ assert.ok(result.stdout.startsWith('OUT\n'));
+ assert.ok(result.stderr.startsWith('ERR\n'));
+ assert.ok(result.stdout.endsWith('stdout summary'));
+ assert.ok(result.stderr.endsWith('stderr summary'));
+ assert.ok(result.stdout.includes('bytes omitted'));
+ assert.ok(result.stderr.includes('bytes omitted'));
+ assert.ok(
+ Buffer.byteLength(result.stdout) + Buffer.byteLength(result.stderr) <=
+ budget
+ );
+ assert.equal(result.truncated, true);
+
+ const shortError = collect(Buffer.from('late failure'), budget);
+ assert.equal(
+ renderCommandOutput(stdout, shortError, budget).stderr,
+ 'late failure'
+ );
+ assert.equal(
+ renderCommandOutput(shortError, stdout, budget).stdout,
+ 'late failure'
+ );
+});
+
+test('budgeting includes UTF-8 boundaries and marker overhead for every small limit', () => {
+ const content = Buffer.from('世界🌍 café Ελληνικά\n'.repeat(100));
+ for (let budget = 1; budget <= 256; budget += 1) {
+ const result = collect(content, budget, 1).render(budget);
+ assertBounded(result.text, budget);
+ assert.equal(result.truncated, true);
+ assert.ok(
+ !result.text.includes('\ufffd'),
+ `split code point at budget ${budget}`
+ );
+ const marker = /\n\[\.\.\. (\d+) bytes omitted \.\.\.\]\n/.exec(
+ result.text
+ );
+ if (marker) {
+ assert.equal(
+ Number(marker[1]),
+ content.length -
+ Buffer.byteLength(result.text.replace(marker[0], ''))
+ );
+ }
+ const streams = renderCommandOutput(
+ collect(content, budget, 7),
+ collect(content, budget, 3),
+ budget
+ );
+ assert.ok(
+ Buffer.byteLength(streams.stdout) +
+ Buffer.byteLength(streams.stderr) <=
+ budget
+ );
+ assert.ok(
+ !streams.stdout.includes('\ufffd') &&
+ !streams.stderr.includes('\ufffd')
+ );
+ }
+});
+
+test('malformed output cannot expand past the byte budget during decoding', () => {
+ for (const content of [
+ Buffer.alloc(1024, 0xff),
+ Buffer.from([0xf0, 0x80, 0xff, 0xc2, 0xa2, 0xe0, 0xa0]),
+ Buffer.from('a\ufffdb'.repeat(300)),
+ ]) {
+ for (let budget = 1; budget <= 128; budget += 1) {
+ const result = collect(content, budget, 1).render(budget);
+ assertBounded(result.text, budget);
+ if (content.length > budget) assert.equal(result.truncated, true);
+ }
+ }
+});
+
+test('short decoded output is preserved when replacement characters fit the combined budget', () => {
+ const budget = 32;
+ assert.deepEqual(
+ renderCommandOutput(
+ collect(Buffer.from([0xff, 0xff]), budget),
+ collect(Buffer.from('child failure'), budget),
+ budget
+ ),
+ { stdout: '\ufffd\ufffd', stderr: 'child failure', truncated: false }
+ );
+});
+
+test('tiny budgets still flag truncation and give stderr the odd byte', () => {
+ for (let budget = 1; budget < 32; budget += 1) {
+ const result = renderCommandOutput(
+ collect(Buffer.from('a'.repeat(256)), budget),
+ collect(Buffer.from('b'.repeat(256)), budget),
+ budget
+ );
+ assert.equal(result.truncated, true);
+ assert.ok(result.stderr.length > 0);
+ assert.ok(
+ Buffer.byteLength(result.stdout) +
+ Buffer.byteLength(result.stderr) <=
+ budget
+ );
+ }
+});
+
+test('copies bounded windows instead of retaining or repeatedly concatenating source chunks', () => {
+ const budget = 128;
+ const output = new OutputBuffer(budget);
+ const oversized = Buffer.alloc(1024 * 1024, 'a');
+ output.append(oversized);
+ oversized.fill('z');
+ const byte = Buffer.from('b');
+ for (let i = 0; i < 100_000; i += 1) output.append(byte);
+ byte[0] = 0x7a;
+ const result = output.render(budget);
+ assert.ok(result.text.startsWith('a'.repeat(40)));
+ assert.ok(result.text.endsWith('b'.repeat(40)));
+ assert.ok(!result.text.includes('z'));
+ assert.equal(output.bytes, 1024 * 1024 + 100_000);
+ assertBounded(result.text, budget);
+});
diff --git a/packages/code/src/output.ts b/packages/code/src/output.ts
new file mode 100644
index 00000000..72e26291
--- /dev/null
+++ b/packages/code/src/output.ts
@@ -0,0 +1,187 @@
+interface OutputWindow {
+ text: string;
+ bytes: number;
+}
+
+function utf8Window(
+ buffer: Buffer,
+ length: number,
+ tail: boolean
+): OutputWindow {
+ let start = tail ? buffer.length - length : 0;
+ let end = tail ? buffer.length : length;
+ if (tail) {
+ while (start < end && (buffer[start] & 0xc0) === 0x80) start += 1;
+ } else if (end > 0) {
+ let last = end - 1;
+ while (last > 0 && (buffer[last] & 0xc0) === 0x80) last -= 1;
+ const lead = buffer[last];
+ const width =
+ lead >= 0xc2 && lead <= 0xdf
+ ? 2
+ : lead >= 0xe0 && lead <= 0xef
+ ? 3
+ : lead >= 0xf0 && lead <= 0xf4
+ ? 4
+ : 1;
+ if (end - last < width) end = last;
+ }
+ return {
+ text: buffer.subarray(start, end).toString('utf8'),
+ bytes: end - start,
+ };
+}
+
+function boundedWindow(
+ buffer: Buffer,
+ budget: number,
+ tail: boolean
+): OutputWindow {
+ const length = Math.min(buffer.length, budget);
+ const window = utf8Window(buffer, length, tail);
+ if (Buffer.byteLength(window.text) <= budget) return window;
+
+ // Malformed bytes expand to replacement characters. Valid UTF-8 takes the fast path.
+ let low = 0;
+ let high = length;
+ while (low < high) {
+ const middle = Math.ceil((low + high) / 2);
+ if (
+ Buffer.byteLength(utf8Window(buffer, middle, tail).text) <= budget
+ ) {
+ low = middle;
+ } else {
+ high = middle - 1;
+ }
+ }
+ return utf8Window(buffer, low, tail);
+}
+
+/** Copies a prefix and rolling suffix, never retaining child-process chunk buffers. */
+export class OutputBuffer {
+ private readonly headCapacity: number;
+ private readonly tailCapacity: number;
+ private head?: Buffer;
+ private tail?: Buffer;
+ private headLength = 0;
+ private tailLength = 0;
+ private tailOffset = 0;
+ private totalBytes = 0;
+
+ constructor(capacity: number) {
+ this.headCapacity = Math.floor(capacity / 2);
+ this.tailCapacity = capacity - this.headCapacity;
+ }
+
+ get bytes(): number {
+ return this.totalBytes;
+ }
+
+ append(chunk: Buffer): void {
+ this.totalBytes += chunk.length;
+ const headBytes = Math.min(
+ chunk.length,
+ this.headCapacity - this.headLength
+ );
+ if (headBytes > 0) {
+ this.head ??= Buffer.allocUnsafe(this.headCapacity);
+ chunk.copy(this.head, this.headLength, 0, headBytes);
+ this.headLength += headBytes;
+ }
+ const remaining = chunk.length - headBytes;
+ if (remaining === 0) return;
+ this.tail ??= Buffer.allocUnsafe(this.tailCapacity);
+ if (remaining >= this.tailCapacity) {
+ chunk.copy(this.tail, 0, chunk.length - this.tailCapacity);
+ this.tailOffset = 0;
+ this.tailLength = this.tailCapacity;
+ return;
+ }
+ const first = Math.min(remaining, this.tailCapacity - this.tailOffset);
+ chunk.copy(this.tail, this.tailOffset, headBytes, headBytes + first);
+ chunk.copy(this.tail, 0, headBytes + first);
+ this.tailOffset = (this.tailOffset + remaining) % this.tailCapacity;
+ this.tailLength = Math.min(
+ this.tailCapacity,
+ this.tailLength + remaining
+ );
+ }
+
+ private suffix(): Buffer {
+ if (!this.tail) return Buffer.alloc(0);
+ if (this.tailLength < this.tailCapacity)
+ return this.tail.subarray(0, this.tailLength);
+ if (this.tailOffset === 0) return this.tail;
+ return Buffer.concat([
+ this.tail.subarray(this.tailOffset),
+ this.tail.subarray(0, this.tailOffset),
+ ]);
+ }
+
+ render(budget: number): { text: string; truncated: boolean } {
+ const head = this.head?.subarray(0, this.headLength) ?? Buffer.alloc(0);
+ const tail = this.suffix();
+ if (this.totalBytes <= budget) {
+ const text = Buffer.concat([head, tail]).toString('utf8');
+ if (Buffer.byteLength(text) <= budget)
+ return { text, truncated: false };
+ }
+ const marker = (bytes: number): string =>
+ `\n[... ${bytes} bytes omitted ...]\n`;
+ const markerBytes = Buffer.byteLength(marker(this.totalBytes));
+ const includeMarker = budget >= markerBytes;
+ const contentBudget = includeMarker ? budget - markerBytes : budget;
+ const prefix = boundedWindow(
+ head,
+ Math.floor(contentBudget / 2),
+ false
+ );
+ const suffix = boundedWindow(tail, Math.ceil(contentBudget / 2), true);
+ const omitted = this.totalBytes - prefix.bytes - suffix.bytes;
+ return {
+ text:
+ prefix.text +
+ (includeMarker ? marker(omitted) : '') +
+ suffix.text,
+ truncated: omitted > 0,
+ };
+ }
+}
+
+/** Each stream can use the whole budget when the other is quiet; stderr cannot be starved. */
+export function renderCommandOutput(
+ stdout: OutputBuffer,
+ stderr: OutputBuffer,
+ budget: number
+): {
+ stdout: string;
+ stderr: string;
+ truncated: boolean;
+} {
+ if (stdout.bytes + stderr.bytes <= budget) {
+ const out = stdout.render(budget);
+ const err = stderr.render(budget);
+ if (
+ Buffer.byteLength(out.text) + Buffer.byteLength(err.text) <=
+ budget
+ ) {
+ return {
+ stdout: out.text,
+ stderr: err.text,
+ truncated: out.truncated || err.truncated,
+ };
+ }
+ }
+ const stderrReserve = Math.ceil(budget / 2);
+ const stdoutBudget = Math.min(
+ stdout.bytes,
+ budget - stderrReserve + Math.max(0, stderrReserve - stderr.bytes)
+ );
+ const out = stdout.render(stdoutBudget);
+ const err = stderr.render(budget - stdoutBudget);
+ return {
+ stdout: out.text,
+ stderr: err.text,
+ truncated: out.truncated || err.truncated,
+ };
+}