From 25fc82d255851c028bdb3831673bcf00aea62dc2 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Sat, 31 Oct 2026 12:24:39 +0900 Subject: [PATCH] Cut a tool's link fields and a vendor's refusal between characters, not inside an emoji --- CHANGELOG.md | 7 +++++++ server/src/plugins/mcp.ts | 4 ++-- server/tests/mcp-result.test.ts | 10 ++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9d3f16f4..113faadb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,13 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. `COMPUTER_MEMORY_HIGH_PERCENT=0` and `COMPUTER_TAB_IDLE_MS=0` (Helm: `computers.memoryHighPercent` and `computers.tabIdleMs`) keep the old behaviour. +### A tool's link and a vendor's refusal are cut between characters + +When a tool answers with a link, its title, name and description are each cut to 400 characters +before the Bot reads them, and the reason a vendor gives for refusing a request is cut to 400 too. +When the cut landed inside an emoji, the Bot was handed a broken character before the "…". The cut +now drops the half character instead. + ### A computer closes browsers nobody is using when its memory runs high A computer runs one browser for each Bot that is browsing, and the only limits were a count and half diff --git a/server/src/plugins/mcp.ts b/server/src/plugins/mcp.ts index 49d9db9959..6175093bdd 100644 --- a/server/src/plugins/mcp.ts +++ b/server/src/plugins/mcp.ts @@ -74,7 +74,7 @@ function resourceLinkText(item: { typeof value === "string" && value.trim() !== "" ? value : null; const bounded = (value: string) => value.length > LINK_FIELD_CHARS - ? `${value.slice(0, LINK_FIELD_CHARS)}…` + ? `${cutAtCodeUnits(value, LINK_FIELD_CHARS)}…` : value; const lines: string[] = []; const uri = field(item.uri); @@ -274,7 +274,7 @@ function reasonFrom(error: unknown): string | null { /** Long enough for a sentence and a URL, short enough not to be the wall of JSON this replaced. */ const trimmed = (value: string) => - value.length > 400 ? `${value.slice(0, 400)}…` : value; + value.length > 400 ? `${cutAtCodeUnits(value, 400)}…` : value; /** * A vendor's failure, as one sentence an operator can act on. diff --git a/server/tests/mcp-result.test.ts b/server/tests/mcp-result.test.ts index a93dbc39c8..0e8232e481 100644 --- a/server/tests/mcp-result.test.ts +++ b/server/tests/mcp-result.test.ts @@ -195,6 +195,16 @@ describe("a result with something in it", () => { expect(text).toBe(`uri: file:///a.md\ndescription: ${"d".repeat(400)}…`); }); + test("cuts a resource_link's long field between characters, not inside an emoji", () => { + // 399 units of text and then an emoji: the 400-unit cut lands between its two halves. + const description = `${"d".repeat(399)}${"🙂".repeat(10)}`; + const { text } = resultText([ + { type: "resource_link", uri: "file:///a.md", description }, + ]); + expect(text).toBe(`uri: file:///a.md\ndescription: ${"d".repeat(399)}…`); + expect(text.isWellFormed()).toBe(true); + }); + test("a resource_link that names nothing is still named, rather than dropped", () => { expect(resultText([{ type: "resource_link" }]).text).toBe( "[resource_link]",