fix(diagnostics-otel): preserve JSON Unicode boundaries (#103646)

* fix(diagnostics-otel): preserve JSON Unicode boundaries

* test(diagnostics-otel): consolidate Unicode boundary coverage

* docs(changelog): credit OTEL Unicode fix

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
mushuiyu886
2026-07-11 00:42:25 +08:00
committed by GitHub
parent 90322074dd
commit 9c9010efca
3 changed files with 10 additions and 4 deletions

View File

@@ -49,7 +49,7 @@ Docs: https://docs.openclaw.ai
- **Apple timeout recovery:** return promptly from shared operation deadlines and caller cancellation even when platform work ignores cancellation, while isolating late Gateway handshakes and cleaning up location and permission waiters. (#103066) Thanks @NianJiuZst.
- **Claude CLI warm sessions:** preserve managed stdio continuity when Claude writes no native transcript, fall back to bounded OpenClaw history only when the exact live child disappears or changes, and keep stateless runs from persisting CLI bindings. (#96841) Thanks @bradreaves.
- **CLI plugin listing:** skip state-migration runtime loading when no legacy inputs exist, reducing packaged cold-start memory while preserving migrations for legacy plugin indexes and configured session stores.
- **Unicode-safe bounded text:** preserve complete UTF-16 surrogate pairs when shortening previews, prompts, diagnostics, labels, session keys, link metadata, and identity values across Control UI, CLI, Gateway, plugins, QA, memory, and Android surfaces. (#102625, #102626, #102627, #102656, #102816, #102823, #102833, #102877, #102949, #102963, #102969, #102988, #103010, #103034, #103210, #103341, #103487, #103543, #103580) Thanks @zhangguiping-xydt, @wings1029, @wangyan2026, @Pandah97, @MoerAI, @SunnyShu0925, @zhangqueping, @zw-xysk, @cxbAsDev, @lzyyzznl, @coder-master-0915, @LeonidasLux, @mushuiyu886, @ly85206559, @Simon-XYDT, and @lsr911.
- **Unicode-safe bounded text:** preserve complete UTF-16 surrogate pairs when shortening previews, prompts, diagnostics, labels, session keys, link metadata, and identity values across Control UI, CLI, Gateway, plugins, QA, memory, and Android surfaces. (#102625, #102626, #102627, #102656, #102816, #102823, #102833, #102877, #102949, #102963, #102969, #102988, #103010, #103034, #103210, #103341, #103487, #103543, #103580, #103646) Thanks @zhangguiping-xydt, @wings1029, @wangyan2026, @Pandah97, @MoerAI, @SunnyShu0925, @zhangqueping, @zw-xysk, @cxbAsDev, @lzyyzznl, @coder-master-0915, @LeonidasLux, @mushuiyu886, @ly85206559, @Simon-XYDT, and @lsr911.
- **CLI model tables:** sanitize, truncate, and pad model-list cells by rendered terminal width so emoji, CJK, and other wide graphemes keep columns aligned. (#102819) Thanks @Kevin23-design and @vincentkoc.
- **Skills prompt compaction:** preserve every included skill identity before using the remaining prompt budget for shortened, UTF-16-safe descriptions, retaining trigger guidance without exceeding the hard limit. (#88426) Thanks @abel-zer0.
- **Channel Markdown code tables:** size columns by rendered display width so CJK, emoji, and mixed-width cells stay aligned across shared Telegram and Discord output. (#55596, #55512) Thanks @sparkyrider.

View File

@@ -5321,6 +5321,9 @@ describe("diagnostics-otel service", () => {
});
await service.start(ctx);
// The 8,192-character candidate budget leaves an 8,178-character text prefix;
// place a surrogate pair across that boundary so serialized JSON must stay valid.
const surrogateBoundaryPrefix = "x".repeat(8177);
emitTrustedModelCallCompletedWithContent(
{
runId: "run-1",
@@ -5333,7 +5336,9 @@ describe("diagnostics-otel service", () => {
inputMessages: [
{
role: "user",
content: `single-message-${"x".repeat(MAX_TEST_OTEL_CONTENT_ATTRIBUTE_CHARS)}`,
content: `${surrogateBoundaryPrefix}🚀${"y".repeat(
MAX_TEST_OTEL_CONTENT_ATTRIBUTE_CHARS,
)}`,
},
],
toolDefinitions: [
@@ -5364,13 +5369,14 @@ describe("diagnostics-otel service", () => {
const toolDefinitions = stringAttribute(attrs, "gen_ai.tool.definitions");
expect(genAiInput.length).toBeLessThanOrEqual(MAX_TEST_OTEL_CONTENT_ATTRIBUTE_CHARS);
expect(toolDefinitions.length).toBeLessThanOrEqual(MAX_TEST_OTEL_CONTENT_ATTRIBUTE_CHARS);
expect(genAiInput).not.toContain("\\ud83d");
expect(JSON.parse(genAiInput)).toEqual([
{
role: "user",
parts: [
{
type: "text",
content: expect.stringContaining("single-message-"),
content: `${surrogateBoundaryPrefix}...(truncated)`,
},
],
},

View File

@@ -935,7 +935,7 @@ function truncateJsonTextForOtelAttribute(value: string, maxChars: number): stri
}
const suffixBudget = Math.min(TRUNCATED_JSON_TEXT_SUFFIX.length, maxChars);
const prefixBudget = Math.max(0, maxChars - suffixBudget);
return `${redacted.slice(0, prefixBudget)}${TRUNCATED_JSON_TEXT_SUFFIX.slice(
return `${truncateUtf16Safe(redacted, prefixBudget)}${TRUNCATED_JSON_TEXT_SUFFIX.slice(
TRUNCATED_JSON_TEXT_SUFFIX.length - suffixBudget,
)}`;
}