Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
465d240
fix(core): truncate model-facing tool output
Jerry2003826 May 25, 2026
a90adf9
fix(core): harden tool output truncation handling
Jerry2003826 May 26, 2026
e77fc08
fix(core): sanitize truncation output telemetry path
Jerry2003826 May 26, 2026
583a7fd
fix(core): sanitize truncation path in otel telemetry
Jerry2003826 May 26, 2026
c8eea3d
test(core): cover non-string tool output truncation
Jerry2003826 May 27, 2026
b695d6a
test(core): assert non-string truncation debug log
Jerry2003826 May 27, 2026
94eb231
test(core): emit telemetry when tool truncation fails
Jerry2003826 May 27, 2026
f319c10
fix(core): use structured tool truncation signal
Jerry2003826 May 27, 2026
35604fc
fix(core): bound truncation fallback paths
Jerry2003826 May 27, 2026
b4d9767
fix(core): cover truncation fallback gaps
Jerry2003826 May 28, 2026
c99135a
fix(core): correlate truncation telemetry with call ids
Jerry2003826 May 28, 2026
dc3ee34
fix(core): report fs error codes in truncation telemetry
Jerry2003826 May 28, 2026
358e4a0
fix(core): harden tool output truncation feedback
Jerry2003826 May 29, 2026
fe0d527
Merge remote-tracking branch 'origin/main' into codex/fix-tool-output…
Jerry2003826 May 29, 2026
a0ee305
fix(core): scope tool output truncation to scheduler strings
Jerry2003826 Jun 1, 2026
2ea9955
Merge remote-tracking branch 'origin/main' into codex/fix-tool-output…
Jerry2003826 Jun 1, 2026
a811f42
fix(core): avoid re-truncating saved tool output
Jerry2003826 Jun 8, 2026
efa179a
Merge remote-tracking branch 'origin/main' into codex/fix-tool-output…
Jerry2003826 Jun 8, 2026
fafe8ed
fix(core): harden tool output truncation
Jerry2003826 Jun 10, 2026
f949b33
fix(core): tighten tool truncation review followups
Jerry2003826 Jun 10, 2026
81498ed
fix(core): log tool truncation fallback metadata
Jerry2003826 Jun 10, 2026
a04b91d
fix(core): resolve merge conflicts with main for PR #4520
cursoragent Jun 14, 2026
7481a83
fix(core): align truncation telemetry with logger fields
cursoragent Jun 14, 2026
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
8 changes: 7 additions & 1 deletion packages/core/src/core/coreToolScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3405,7 +3405,12 @@ export class CoreToolScheduler {
this.config,
toolName,
content,
{ threshold: perToolMax, lines: perToolLines, keep: perToolKeep },
{
threshold: perToolMax,
lines: perToolLines,
keep: perToolKeep,
callId,
},
promptIdForTruncation,
);
content = truncated.content;
Expand Down Expand Up @@ -3460,6 +3465,7 @@ export class CoreToolScheduler {
threshold: baseThreshold * 2,
lines: combinedLines,
keep: perToolKeep,
callId,
},
promptIdForTruncation,
);
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/telemetry/loggers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1311,11 +1311,15 @@ describe('loggers', () => {

it('should log a tool output truncated event', () => {
const event = new ToolOutputTruncatedEvent('prompt-id-1', {
callId: 'call-id-1',
toolName: 'test-tool',
originalContentLength: 1000,
truncatedContentLength: 100,
threshold: 500,
lines: 10,
outputFileSaved: false,
saveErrorCode: 'EACCES',
saveErrorMessage: 'permission denied',
});

logToolOutputTruncated(mockConfig, event);
Expand All @@ -1328,11 +1332,15 @@ describe('loggers', () => {
'event.timestamp': '2025-01-01T00:00:00.000Z',
eventName: 'tool_output_truncated',
prompt_id: 'prompt-id-1',
call_id: 'call-id-1',
tool_name: 'test-tool',
original_content_length: 1000,
truncated_content_length: 100,
threshold: 500,
lines: 10,
output_file_saved: false,
save_error_code: 'EACCES',
save_error_message: 'permission denied',
},
});
});
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/telemetry/qwen-logger/qwen-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,10 +595,14 @@ export class QwenLogger {
tool_name: event.tool_name,
},
snapshots: JSON.stringify({
call_id: event.call_id,
original_content_length: event.original_content_length,
truncated_content_length: event.truncated_content_length,
threshold: event.threshold,
lines: event.lines,
output_file_saved: event.output_file_saved,
save_error_code: event.save_error_code,
save_error_message: event.save_error_message,
}),
});

Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,24 +765,36 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent {
threshold: number;
lines: number;
prompt_id: string;
call_id?: string;
output_file_saved: boolean;
save_error_code?: string;
save_error_message?: string;

constructor(
prompt_id: string,
details: {
callId?: string;
toolName: string;
originalContentLength: number;
truncatedContentLength: number;
threshold: number;
lines: number;
outputFileSaved?: boolean;
saveErrorCode?: string;
saveErrorMessage?: string;
},
) {
this['event.name'] = this.eventName;
this.prompt_id = prompt_id;
this.call_id = details.callId;
this.tool_name = details.toolName;
this.original_content_length = details.originalContentLength;
this.truncated_content_length = details.truncatedContentLength;
this.threshold = details.threshold;
this.lines = details.lines;
this.output_file_saved = details.outputFileSaved ?? true;
this.save_error_code = details.saveErrorCode;
this.save_error_message = details.saveErrorMessage;
}
}

Expand Down
69 changes: 14 additions & 55 deletions packages/core/src/tools/shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1450,62 +1450,21 @@ describe('ShellTool', () => {
expect(result.llmContent).not.toContain('foreground command ran for');
});

it('appends the hint AFTER truncation (so it survives `truncateToolOutput`)', async () => {
// `truncateToolOutput` wraps over-budget output in a "Truncated
// part of the output:" envelope. If the hint were appended
// inside that envelope (i.e. before truncation), the LLM might
// read the advisory as part of the command's own output. Pin
// the post-truncation insertion order: the hint must appear
// outside the truncation marker.
//
// Mock `truncateToolOutput` directly rather than driving real
// truncation — the real path needs `fs.writeFile` to actually
// succeed (the catch fallback returns no `outputFile`, so the
// shell.ts replacement branch never fires). Mocking here pins
// ordering, which is all this test cares about.
const truncationModule = await import('../utils/truncation.js');
const spy = vi
.spyOn(truncationModule, 'truncateToolOutput')
.mockResolvedValue({
content:
'Tool output was too large and has been truncated.\n[mocked truncated body]',
outputFile: '/tmp/qwen-temp/shell_mocked.output',
});

try {
const invocation = shellTool.build({
command: 'long-output-cmd',
is_background: false,
});
const promise = invocation.execute(mockAbortSignal);
await vi.advanceTimersByTimeAsync(60_000);
resolveShellExecution({ output: 'A'.repeat(500), exitCode: 0 });
const result = await promise;
it('appends the hint after command output is assembled', async () => {
const invocation = shellTool.build({
command: 'long-output-cmd',
is_background: false,
});
const promise = invocation.execute(mockAbortSignal);
await vi.advanceTimersByTimeAsync(60_000);
resolveShellExecution({ output: 'A'.repeat(500), exitCode: 0 });
const result = await promise;

const content = result.llmContent as string;
// Hint present.
expect(content).toContain('foreground command ran for 60s');
// Truncation envelope present (proves the truncation branch
// actually ran in shell.ts — `outputFile` was set so the
// replacement happened).
expect(content).toContain(
'Tool output was too large and has been truncated.',
);
// Hint comes AFTER the truncation marker — pins the
// post-truncation insertion order so a regression that
// moves the append back inside the non-aborted llmContent
// builder (where it'd get wrapped by the truncation
// envelope on long output) would fail loudly.
const truncIdx = content.indexOf(
'Tool output was too large and has been truncated.',
);
const hintIdx = content.indexOf('foreground command ran for');
expect(hintIdx).toBeGreaterThan(truncIdx);
} finally {
// Restore even if assertions throw — otherwise the
// truncateToolOutput spy leaks into subsequent tests.
spy.mockRestore();
}
const content = result.llmContent as string;
const outputIdx = content.indexOf('A'.repeat(20));
const hintIdx = content.indexOf('foreground command ran for');
expect(outputIdx).toBeGreaterThanOrEqual(0);
expect(hintIdx).toBeGreaterThan(outputIdx);
});

it('truncates shell output char-only so the line cap cannot undercut the char budget', async () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/utils/truncation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export async function truncateToolOutput(
threshold?: number;
lines?: number;
keep?: 'head' | 'tail' | 'both';
callId?: string;
},
promptId?: string,
): Promise<{ content: string; outputFile?: string }> {
Expand Down Expand Up @@ -234,16 +235,18 @@ export async function truncateToolOutput(
keep,
);

if (result.outputFile) {
if (result.content !== content) {
try {
logToolOutputTruncated(
config,
new ToolOutputTruncatedEvent(promptId ?? '', {
callId: limits?.callId,
toolName,
originalContentLength: originalLength,
truncatedContentLength: result.content.length,
threshold,
lines,
outputFileSaved: Boolean(result.outputFile),
}),
);
} catch {
Expand Down Expand Up @@ -271,6 +274,7 @@ export async function truncateLlmContent(
threshold?: number;
lines?: number;
keep?: 'head' | 'tail' | 'both';
callId?: string;
},
promptId?: string,
): Promise<{ content: PartListUnion; outputFile?: string }> {
Expand Down
Loading