Skip to content

Commit 45447e9

Browse files
committed
feat(tools): extract shared truncateOutput utility and apply to diagnostic output
Moved truncateOutput and formatBytes from shell.ts to new shared truncate.ts module. Applied truncation to type error diagnostics in editFile, multiEdit, and writeFile tools (4KB limit) to prevent excessively large tool responses.
1 parent 9c7ad6c commit 45447e9

5 files changed

Lines changed: 44 additions & 37 deletions

File tree

src/backend/tools/base/truncate.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Shared output truncation utilities
3+
*/
4+
5+
/** Size of head portion to preserve when truncating */
6+
const HEAD_SIZE = 1024; // 1KB
7+
8+
/** Size of tail portion to preserve when truncating */
9+
const TAIL_SIZE = 3072; // 3KB
10+
11+
/**
12+
* Format byte count for display (KB with one decimal for >= 1024, bytes otherwise)
13+
*/
14+
export function formatBytes(bytes: number): string {
15+
if (bytes >= 1024) {
16+
return `${(bytes / 1024).toFixed(1)}KB`;
17+
}
18+
return `${bytes} bytes`;
19+
}
20+
21+
/**
22+
* Truncate output using head+tail strategy if it exceeds max size.
23+
* Uses HEAD_SIZE:TAIL_SIZE ratio (1:3) scaled to maxSize for truncation bounds.
24+
*/
25+
export function truncateOutput(output: string, maxSize: number): string {
26+
if (output.length <= maxSize) {
27+
return output;
28+
}
29+
// Scale head/tail sizes proportionally to maxSize, maintaining 1:3 ratio
30+
const totalParts = HEAD_SIZE + TAIL_SIZE; // 4KB
31+
const headSize = Math.floor((HEAD_SIZE / totalParts) * maxSize);
32+
const tailSize = maxSize - headSize;
33+
34+
const omittedBytes = output.length - headSize - tailSize;
35+
const omissionMsg = `\n... [${formatBytes(omittedBytes)} omitted] ...\n`;
36+
return output.slice(0, headSize) + omissionMsg + output.slice(-tailSize);
37+
}

src/backend/tools/pulsing/editFile.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
66
import { isAbsolute, join, normalize } from "node:path";
77
import { z } from "zod";
88
import { log } from "@/backend/logger";
9+
import { truncateOutput } from "../base/truncate";
910
import { getEffectiveRoot } from "../base/utils";
1011
import {
1112
REASON_DESCRIPTION,
@@ -202,7 +203,7 @@ Note: You are working in an isolated git worktree. Changes are isolated until pu
202203
let diagnosticOutput = "";
203204
const diagnostics = await getDiagnostics(context, fullPath);
204205
if (diagnostics) {
205-
diagnosticOutput = `\n\n⚠️ Type errors:\n${diagnostics}`;
206+
diagnosticOutput = `\n\n⚠️ Type errors:\n${truncateOutput(diagnostics, 4 * 1024)}`;
206207
}
207208

208209
// Build output with hook output appended if non-empty

src/backend/tools/pulsing/multiEdit.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
66
import { isAbsolute, join, normalize } from "node:path";
77
import { z } from "zod";
88
import { log } from "@/backend/logger";
9+
import { truncateOutput } from "../base/truncate";
910
import { getEffectiveRoot } from "../base/utils";
1011
import {
1112
REASON_DESCRIPTION,
@@ -250,7 +251,7 @@ Note: You are working in an isolated git worktree. Changes are isolated until pu
250251
let diagnosticOutput = "";
251252
const diagnostics = await getDiagnostics(context, fullPath);
252253
if (diagnostics) {
253-
diagnosticOutput = `\n\n⚠️ Type errors:\n${diagnostics}`;
254+
diagnosticOutput = `\n\n⚠️ Type errors:\n${truncateOutput(diagnostics, 4 * 1024)}`;
254255
}
255256

256257
// Extract context using shared helper with range merging

src/backend/tools/pulsing/shell.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { z } from "zod";
66
import { log } from "@/backend/logger";
77
import { shellApprovalService } from "@/backend/services/shell-approval";
88
import { collectPartialOutput, dumpTimeoutLog } from "../base/timeoutLog";
9+
import { truncateOutput } from "../base/truncate";
910
import { getEffectiveRoot } from "../base/utils";
1011
import {
1112
REASON_DESCRIPTION,
@@ -23,12 +24,6 @@ const FULL_OUTPUT_SIZE = 64 * 1024; // 64KB
2324
/** Default output size for truncated output */
2425
const DEFAULT_OUTPUT_SIZE = 4 * 1024; // 4KB
2526

26-
/** Size of head portion to preserve when truncating */
27-
const HEAD_SIZE = 1024; // 1KB
28-
29-
/** Size of tail portion to preserve when truncating */
30-
const TAIL_SIZE = 3072; // 3KB
31-
3227
// =============================================================================
3328
// Schema
3429
// =============================================================================
@@ -59,34 +54,6 @@ export type ShellInput = z.infer<typeof shellInputSchema>;
5954
// Helper Functions
6055
// =============================================================================
6156

62-
/**
63-
* Format byte count for display (KB with one decimal for >= 1024, bytes otherwise)
64-
*/
65-
function formatBytes(bytes: number): string {
66-
if (bytes >= 1024) {
67-
return `${(bytes / 1024).toFixed(1)}KB`;
68-
}
69-
return `${bytes} bytes`;
70-
}
71-
72-
/**
73-
* Truncate output using head+tail strategy if it exceeds max size.
74-
* Uses HEAD_SIZE:TAIL_SIZE ratio (1:3) scaled to maxSize for truncation bounds.
75-
*/
76-
function truncateOutput(output: string, maxSize: number): string {
77-
if (output.length <= maxSize) {
78-
return output;
79-
}
80-
// Scale head/tail sizes proportionally to maxSize, maintaining 1:3 ratio
81-
const totalParts = HEAD_SIZE + TAIL_SIZE; // 4KB
82-
const headSize = Math.floor((HEAD_SIZE / totalParts) * maxSize);
83-
const tailSize = maxSize - headSize;
84-
85-
const omittedBytes = output.length - headSize - tailSize;
86-
const omissionMsg = `\n... [${formatBytes(omittedBytes)} omitted] ...\n`;
87-
return output.slice(0, headSize) + omissionMsg + output.slice(-tailSize);
88-
}
89-
9057
/**
9158
* Get the shell command args for the current platform
9259
*/

src/backend/tools/pulsing/writeFile.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { dirname, isAbsolute, join, normalize } from "node:path";
1313
import { z } from "zod";
1414
import { log } from "@/backend/logger";
15+
import { truncateOutput } from "../base/truncate";
1516
import { getEffectiveRoot } from "../base/utils";
1617
import {
1718
REASON_DESCRIPTION,
@@ -110,7 +111,7 @@ Note: You are working in an isolated git worktree. Changes are isolated until pu
110111
let diagnosticOutput = "";
111112
const diagnostics = await getDiagnostics(context, fullPath);
112113
if (diagnostics) {
113-
diagnosticOutput = `\n\n⚠️ Type errors:\n${diagnostics}`;
114+
diagnosticOutput = `\n\n⚠️ Type errors:\n${truncateOutput(diagnostics, 4 * 1024)}`;
114115
}
115116

116117
// Build output with hook output appended if non-empty

0 commit comments

Comments
 (0)