feat: add truncation to Task tool output and auto-cleanup overflow files (#588)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-18 14:16:50 -08:00
committed by GitHub
parent dac6d77593
commit d96ba6dd2e
5 changed files with 72 additions and 6 deletions

View File

@@ -362,6 +362,16 @@ async function main(): Promise<void> {
// Silently ignore update failures
});
// Clean up old overflow files (non-blocking, 24h retention)
const { cleanupOldOverflowFiles } = await import("./tools/impl/overflow");
Promise.resolve().then(() => {
try {
cleanupOldOverflowFiles(process.cwd());
} catch {
// Silently ignore cleanup failures
}
});
// Parse command-line arguments (Bun-idiomatic approach using parseArgs)
// Preprocess args to support --conv as alias for --conversation
const processedArgs = process.argv.map((arg) =>

View File

@@ -170,6 +170,33 @@ describe("overflow utilities", () => {
expect(deletedCount).toBe(0);
});
test("skips subdirectories without crashing", () => {
// Create a test file and a subdirectory
const content = "Test content";
const filePath = writeOverflowFile(content, testWorkingDir, "TestTool");
// Create a subdirectory in the overflow dir
const subDir = path.join(path.dirname(filePath), "subdir");
fs.mkdirSync(subDir, { recursive: true });
// Make the file old
const oldTime = Date.now() - 48 * 60 * 60 * 1000;
fs.utimesSync(filePath, new Date(oldTime), new Date(oldTime));
// Cleanup should skip the directory and only delete the file
const deletedCount = cleanupOldOverflowFiles(
testWorkingDir,
24 * 60 * 60 * 1000,
);
expect(deletedCount).toBe(1);
expect(fs.existsSync(filePath)).toBe(false);
expect(fs.existsSync(subDir)).toBe(true);
// Clean up the subdir
fs.rmdirSync(subDir);
});
});
describe("getOverflowStats", () => {

View File

@@ -16,6 +16,7 @@ import {
generateSubagentId,
registerSubagent,
} from "../../cli/helpers/subagentState.js";
import { LIMITS, truncateByChars } from "./truncation.js";
import { validateRequiredParams } from "./validation";
interface TaskArgs {
@@ -114,7 +115,18 @@ export async function task(args: TaskArgs): Promise<string> {
.filter(Boolean)
.join(" ");
return `${header}\n\n${result.report}`;
const fullOutput = `${header}\n\n${result.report}`;
const userCwd = process.env.USER_CWD || process.cwd();
// Apply truncation to prevent excessive token usage (same pattern as Bash tool)
const { content: truncatedOutput } = truncateByChars(
fullOutput,
LIMITS.TASK_OUTPUT_CHARS,
"Task",
{ workingDirectory: userCwd, toolName: "Task" },
);
return truncatedOutput;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
completeSubagent(subagentId, { success: false, error: errorMessage });

View File

@@ -113,17 +113,33 @@ export function cleanupOldOverflowFiles(
return 0;
}
const files = fs.readdirSync(overflowDir);
let files: string[];
try {
files = fs.readdirSync(overflowDir);
} catch {
// Directory may have been deleted or become inaccessible
return 0;
}
const now = Date.now();
let deletedCount = 0;
for (const file of files) {
const filePath = path.join(overflowDir, file);
const stats = fs.statSync(filePath);
try {
const stats = fs.statSync(filePath);
if (now - stats.mtimeMs > maxAgeMs) {
fs.unlinkSync(filePath);
deletedCount++;
// Skip directories (shouldn't exist, but be safe)
if (stats.isDirectory()) {
continue;
}
if (now - stats.mtimeMs > maxAgeMs) {
fs.unlinkSync(filePath);
deletedCount++;
}
} catch {
// File may have been deleted, or permission error - skip it
}
}

View File

@@ -10,6 +10,7 @@ import { OVERFLOW_CONFIG, writeOverflowFile } from "./overflow.js";
export const LIMITS = {
// Command output limits
BASH_OUTPUT_CHARS: 30_000, // 30K characters for bash/shell output
TASK_OUTPUT_CHARS: 30_000, // 30K characters for subagent task output
// File reading limits
READ_MAX_LINES: 2_000, // Max lines per file read