Skip to content

Commit e4e2979

Browse files
shaynhornikclaude
andcommitted
refactor: route the remaining code-unit clips through truncateUtf16Safe
The MCP header-warning ellipsis in sep-2243 and the Telegram draft/chunk prefix were two more hand-written copies of the same boundary rule on a UTF-16 code-unit budget. Both now call the shared helper, which retires the Telegram-local prefix walk and its direct test; the same cases live in text-sanitize.test.ts. thread-search's capCodePoints stays as is: its budget is code points, not code units, so it is a different contract rather than another copy. Generated-by: Claude Code (Fable 5.1) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTqr6ZjXfNthXT719ag7Qc
1 parent e04bdeb commit e4e2979

3 files changed

Lines changed: 7 additions & 39 deletions

File tree

packages/mcp/src/sep-2243.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* under the License.
1818
*/
1919

20+
import { truncateUtf16Safe } from '@maka/core/text-sanitize';
2021
import { formatMcpDiagnosticText } from './diagnostic-text.js';
2122

2223
const X_MCP_HEADER = 'x-mcp-header';
@@ -281,9 +282,8 @@ function valueAtPath(root: unknown, path: readonly string[]): unknown {
281282

282283
function truncateRenderedWarning(value: string): string {
283284
if (value.length <= MCP_HEADER_WARNING_LENGTH_LIMIT) return value;
284-
const end = MCP_HEADER_WARNING_LENGTH_LIMIT - 1;
285-
const safeEnd = end > 0 && /[\uD800-\uDBFF]/u.test(value[end - 1] ?? '') ? end - 1 : end;
286-
return `${value.slice(0, safeEnd)}\u2026`;
285+
// The ellipsis is one BMP code unit, so it fits in the unit it reclaims.
286+
return `${truncateUtf16Safe(value, MCP_HEADER_WARNING_LENGTH_LIMIT - 1)}\u2026`;
287287
}
288288

289289
function isRecord(value: unknown): value is Record<string, unknown> {

packages/runtime/src/bots/__tests__/telegram-utf16.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,9 @@ import { describe, it } from 'node:test';
2222

2323
import { __TEST__ } from '../telegram-bridge.js';
2424

25-
const { utf16Len, prefixWithinUtf16, splitForTelegram } = __TEST__;
25+
const { utf16Len, splitForTelegram } = __TEST__;
2626

2727
describe('Telegram UTF-16 limits', () => {
28-
it('truncates prefixes without splitting surrogate pairs', () => {
29-
for (const [text, limit, expected] of [
30-
['hello', 100, 'hello'],
31-
['abcdef', 3, 'abc'],
32-
['a😀', 2, 'a'],
33-
] as const) {
34-
assert.equal(prefixWithinUtf16(text, limit), expected, text);
35-
}
36-
});
37-
3828
it('splits oversized text within limits, on code points, and preferably at newlines', () => {
3929
assert.deepEqual(splitForTelegram('hello world'), ['hello world']);
4030

packages/runtime/src/bots/telegram-bridge.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import type { BotAttachmentKind } from '@maka/core/bot-events';
2929
import type { BotChannelSettings } from '@maka/core/bot-chat-settings';
3030
import { generalizedErrorMessage } from '@maka/core/redaction';
31+
import { truncateUtf16Safe } from '@maka/core/text-sanitize';
3132
import { BaseBotAdapter, botReadinessFromSettings } from './base-adapter.js';
3233
import type {
3334
BotPlatform,
@@ -63,28 +64,6 @@ function utf16Len(s: string): number {
6364
return s.length === 0 ? 0 : Buffer.byteLength(s, 'utf16le') / 2;
6465
}
6566

66-
/**
67-
* Return the longest prefix of `s` whose UTF-16 length is ≤ `cap`,
68-
* respecting surrogate-pair boundaries (we never slice a
69-
* multi-code-unit character in half). We iterate codepoint-by-
70-
* codepoint instead of binary-searching slices: the cost of
71-
* mistakenly splitting an emoji is far worse than the O(n) cost.
72-
*/
73-
function prefixWithinUtf16(s: string, cap: number): string {
74-
if (utf16Len(s) <= cap) return s;
75-
let used = 0;
76-
let end = 0;
77-
for (let i = 0; i < s.length; ) {
78-
const code = s.codePointAt(i)!;
79-
const units = code > 0xffff ? 2 : 1;
80-
if (used + units > cap) break;
81-
used += units;
82-
i += units;
83-
end = i;
84-
}
85-
return s.slice(0, end);
86-
}
87-
8867
/**
8968
* Split `text` into UTF-16-bounded chunks for Telegram delivery.
9069
* Prefers breaking on a newline within the last ~10% of the chunk;
@@ -100,7 +79,7 @@ function splitForTelegram(text: string): string[] {
10079
const pieces: string[] = [];
10180
let remaining = text;
10281
while (utf16Len(remaining) > cap) {
103-
let chunk = prefixWithinUtf16(remaining, cap);
82+
let chunk = truncateUtf16Safe(remaining, cap);
10483
const minBoundary = Math.floor(chunk.length * 0.9);
10584
const nl = chunk.lastIndexOf('\n');
10685
if (nl >= minBoundary) chunk = chunk.slice(0, nl);
@@ -264,7 +243,6 @@ function classifyTelegramSendResponse(response: any): TelegramSendClassification
264243

265244
export const __TEST__ = {
266245
utf16Len,
267-
prefixWithinUtf16,
268246
splitForTelegram,
269247
buildTelegramSendBody,
270248
normalizeTelegramReplyToMessageId,
@@ -374,7 +352,7 @@ export class TelegramBotBridge extends BaseBotAdapter implements SendCapable {
374352
return createTelegramReplyStream({
375353
chatId,
376354
streamId: options.streamId,
377-
prepareDraftText: (text) => prefixWithinUtf16(text, TELEGRAM_MAX_UTF16_PER_MESSAGE),
355+
prepareDraftText: (text) => truncateUtf16Safe(text, TELEGRAM_MAX_UTF16_PER_MESSAGE),
378356
sendDraft: async (draftId, text) => {
379357
const response = await telegramApi(token, 'sendMessageDraft', {
380358
chat_id: privateChatId,

0 commit comments

Comments
 (0)