From c3b2142fb9844f1fe8a840b71e098359b4788eeb Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Mon, 2 Mar 2026 12:52:31 +0000 Subject: [PATCH 1/3] fix: replace echoed text with bytesWritten in writeTextFile tool result The writeTextFile tool was returning the written text (truncated to 30K chars) back to the LLM, which already knows what it wrote. Replace with bytesWritten to save tokens while still confirming the write succeeded. Co-Authored-By: Claude Opus 4.6 --- .changeset/write-text-file-drop-echo.md | 5 +++++ apps/base/src/tools/write-text-file.test.ts | 22 ++++++++++----------- apps/base/src/tools/write-text-file.ts | 3 +-- 3 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 .changeset/write-text-file-drop-echo.md diff --git a/.changeset/write-text-file-drop-echo.md b/.changeset/write-text-file-drop-echo.md new file mode 100644 index 00000000..b8cde667 --- /dev/null +++ b/.changeset/write-text-file-drop-echo.md @@ -0,0 +1,5 @@ +--- +"@perstack/base": patch +--- + +Replace echoed text with bytesWritten in writeTextFile tool result diff --git a/apps/base/src/tools/write-text-file.test.ts b/apps/base/src/tools/write-text-file.test.ts index 1bf791f0..0c4961f3 100644 --- a/apps/base/src/tools/write-text-file.test.ts +++ b/apps/base/src/tools/write-text-file.test.ts @@ -2,7 +2,6 @@ import { afterEach, describe, expect, it } from "bun:test" import { existsSync } from "node:fs" import fs from "node:fs/promises" import { join } from "node:path" -import { MAX_TOOL_OUTPUT_CHARS } from "@perstack/core" import { validatePath } from "../lib/path.js" import { writeTextFile } from "./write-text-file.js" @@ -19,7 +18,7 @@ describe("writeTextFile tool", () => { expect(existsSync(testFile)).toBe(true) expect(await fs.readFile(testFile, "utf-8")).toBe(content) expect(result.path).toBe(await validatePath(testFile)) - expect(result.text).toBe(content) + expect(result.bytesWritten).toBe(Buffer.byteLength(content, "utf-8")) }) it("overwrites existing file", async () => { @@ -28,7 +27,7 @@ describe("writeTextFile tool", () => { const result = await writeTextFile({ path: testFile, text: newContent }) expect(await fs.readFile(testFile, "utf-8")).toBe(newContent) expect(result.path).toBe(await validatePath(testFile)) - expect(result.text).toBe(newContent) + expect(result.bytesWritten).toBe(Buffer.byteLength(newContent, "utf-8")) }) it("creates file in nested directory", async () => { @@ -38,23 +37,22 @@ describe("writeTextFile tool", () => { const result = await writeTextFile({ path: nestedFile, text: content }) expect(existsSync(nestedFile)).toBe(true) expect(await fs.readFile(nestedFile, "utf-8")).toBe(content) - expect(result.text).toBe(content) + expect(result.bytesWritten).toBe(Buffer.byteLength(content, "utf-8")) }) it("handles empty content", async () => { const result = await writeTextFile({ path: testFile, text: "" }) expect(existsSync(testFile)).toBe(true) expect(await fs.readFile(testFile, "utf-8")).toBe("") - expect(result.text).toBe("") + expect(result.bytesWritten).toBe(0) }) - it("truncates echoed text exceeding 30K characters", async () => { - const largeText = "B".repeat(50_000) - const result = await writeTextFile({ path: testFile, text: largeText }) - expect(result.text.length).toBeLessThanOrEqual(MAX_TOOL_OUTPUT_CHARS) - expect(result.text).toContain("... [truncated:") - // File itself should contain the full content - expect(await fs.readFile(testFile, "utf-8")).toBe(largeText) + it("returns correct bytesWritten for multi-byte characters", async () => { + const content = "こんにちは世界" + const result = await writeTextFile({ path: testFile, text: content }) + expect(await fs.readFile(testFile, "utf-8")).toBe(content) + expect(result.bytesWritten).toBe(Buffer.byteLength(content, "utf-8")) + expect(result.bytesWritten).toBeGreaterThan(content.length) }) it("throws error if existing file is not writable", async () => { diff --git a/apps/base/src/tools/write-text-file.ts b/apps/base/src/tools/write-text-file.ts index 784554a0..6f613dbb 100644 --- a/apps/base/src/tools/write-text-file.ts +++ b/apps/base/src/tools/write-text-file.ts @@ -1,7 +1,6 @@ import { mkdir, stat } from "node:fs/promises" import { dirname } from "node:path" import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" -import { truncateText } from "@perstack/core" import { z } from "zod/v4" import { validatePath } from "../lib/path.js" import { safeWriteFile } from "../lib/safe-file.js" @@ -19,7 +18,7 @@ export async function writeTextFile(input: { path: string; text: string }) { await safeWriteFile(validatedPath, text) return { path: validatedPath, - text: truncateText(text), + bytesWritten: Buffer.byteLength(text, "utf-8"), } } From 102eb74d66d27fd89aad8f436d649f30ea3aede3 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Mon, 2 Mar 2026 12:53:52 +0000 Subject: [PATCH 2/3] chore: add perstack patch bump to changeset Co-Authored-By: Claude Opus 4.6 --- .changeset/write-text-file-drop-echo.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/write-text-file-drop-echo.md b/.changeset/write-text-file-drop-echo.md index b8cde667..16acee78 100644 --- a/.changeset/write-text-file-drop-echo.md +++ b/.changeset/write-text-file-drop-echo.md @@ -1,5 +1,6 @@ --- "@perstack/base": patch +"perstack": patch --- Replace echoed text with bytesWritten in writeTextFile tool result From 3292e9e182e53f98effa9749f9abe3be7bd1bd3d Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Mon, 2 Mar 2026 12:54:07 +0000 Subject: [PATCH 3/3] fix: use ASCII test string for multi-byte character test All artifacts must be written in English per project conventions. Co-Authored-By: Claude Opus 4.6 --- apps/base/src/tools/write-text-file.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/base/src/tools/write-text-file.test.ts b/apps/base/src/tools/write-text-file.test.ts index 0c4961f3..99bab03e 100644 --- a/apps/base/src/tools/write-text-file.test.ts +++ b/apps/base/src/tools/write-text-file.test.ts @@ -48,7 +48,7 @@ describe("writeTextFile tool", () => { }) it("returns correct bytesWritten for multi-byte characters", async () => { - const content = "こんにちは世界" + const content = "hello world \u00e9\u00e0\u00fc" const result = await writeTextFile({ path: testFile, text: content }) expect(await fs.readFile(testFile, "utf-8")).toBe(content) expect(result.bytesWritten).toBe(Buffer.byteLength(content, "utf-8"))