Skip to content

fix: tool summaries preserve emoji truncation boundaries#98644

Merged
vincentkoc merged 1 commit into
openclaw:mainfrom
ZengWen-DT:fix/tool-summary-utf16-pr
Jul 1, 2026
Merged

fix: tool summaries preserve emoji truncation boundaries#98644
vincentkoc merged 1 commit into
openclaw:mainfrom
ZengWen-DT:fix/tool-summary-utf16-pr

Conversation

@ZengWen-DT

@ZengWen-DT ZengWen-DT commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where users viewing tool catalogs or /tools verbose output could see a replacement character when a tool description was truncated at the middle of an emoji or other UTF-16 surrogate pair.

This affects compact tool summaries in the effective tool inventory and Gateway tools catalog, plus verbose tool descriptions shown in /tools verbose.

Why This Change Was Made

The tool description summary helper now uses the existing UTF-16-safe truncation helper before appending ellipses. This keeps the existing whitespace and word-boundary behavior while avoiding dangling surrogate halves in user-visible tool text.

AI-assisted.

User Impact

Tool descriptions that contain emoji or other astral Unicode characters now truncate cleanly instead of rendering broken replacement characters in tool inventory surfaces.

Evidence

Before/premise:

The current raw slice(0, maxLen - 3) behavior can split a surrogate pair. A negative-control run temporarily restored raw slice in src/agents/tool-description-summary.ts and ran the new regression test:

FAIL src/agents/tool-description-summary.test.ts > keeps compact summaries UTF-16 safe at truncation boundaries
Expected: "abcd..."
Received: "abcd�..."

FAIL src/agents/tool-description-summary.test.ts > keeps verbose descriptions UTF-16 safe at truncation boundaries
Expected: "abcd..."
Received: "abcd�..."

After fix, I also ran a local OpenClaw tool-catalog proof with temporary plugin-style tool entries whose descriptions place 😀 exactly on the default truncation boundaries used by Gateway tools.catalog summaries and /tools verbose descriptions:

node --import tsx --input-type=module <<'EOF'
import { buildToolsMessage } from './src/auto-reply/status.ts';
import { summarizeToolDescriptionText } from './src/agents/tool-description-summary.ts';

const hasDanglingSurrogate = (value) => /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value);
const catalogRawDescription = `${'a'.repeat(116)}😀${'b'.repeat(20)}`;
const verboseRawDescription = `${'a'.repeat(316)}😀${'b'.repeat(20)}`;
const catalogDescription = summarizeToolDescriptionText({ rawDescription: catalogRawDescription });
const toolsVerbose = buildToolsMessage({
  profile: 'full',
  groups: [
    {
      id: 'proof',
      label: 'Proof tools',
      source: 'plugin',
      tools: [
        {
          id: 'emoji_catalog_boundary_tool',
          label: 'Emoji Catalog Boundary Tool',
          description: catalogDescription,
          rawDescription: catalogRawDescription,
          source: 'plugin',
          pluginId: 'proof-tools',
        },
        {
          id: 'emoji_verbose_boundary_tool',
          label: 'Emoji Verbose Boundary Tool',
          description: 'Verbose proof tool',
          rawDescription: verboseRawDescription,
          source: 'plugin',
          pluginId: 'proof-tools',
        },
      ],
    },
  ],
}, { verbose: true });
const verboseLine = toolsVerbose.split('\n').find((line) => line.includes('Emoji Verbose Boundary Tool')) ?? '';

console.log('Gateway tools.catalog description projection:');
console.log(JSON.stringify({ id: 'emoji_catalog_boundary_tool', description: catalogDescription }, null, 2));
console.log(`catalog contains replacement char: ${catalogDescription.includes('�')}`);
console.log(`catalog contains dangling surrogate: ${hasDanglingSurrogate(catalogDescription)}`);
console.log('\n/tools verbose output excerpt:');
console.log(verboseLine);
console.log(`verbose contains replacement char: ${verboseLine.includes('�')}`);
console.log(`verbose contains dangling surrogate: ${hasDanglingSurrogate(verboseLine)}`);
EOF

Gateway tools.catalog description projection:
{
  "id": "emoji_catalog_boundary_tool",
  "description": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
}
catalog contains replacement char: false
catalog contains dangling surrogate: false

/tools verbose output excerpt:
  Emoji Verbose Boundary Tool - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
verbose contains replacement char: false
verbose contains dangling surrogate: false

Focused tests and checks:

node scripts/run-vitest.mjs src/agents/tool-description-summary.test.ts src/auto-reply/status.tools.test.ts src/gateway/server-methods/tools-catalog.test.ts

Test Files  1 passed (1)
Tests  2 passed (2)
Test Files  2 passed (2)
Tests  14 passed (14)
Test Files  1 passed (1)
Tests  6 passed (6)

Additional checks:

pnpm format:check -- src/agents/tool-description-summary.ts src/agents/tool-description-summary.test.ts
All matched files use the correct format.

OPENCLAW_OXLINT_SKIP_PREPARE=1 node scripts/run-oxlint.mjs src/agents/tool-description-summary.ts src/agents/tool-description-summary.test.ts
passed

.agents/skills/autoreview/scripts/autoreview --mode local --base upstream/main
autoreview clean: no accepted/actionable findings reported

The standard oxlint wrapper without OPENCLAW_OXLINT_SKIP_PREPARE=1 was also attempted, but current upstream/main failed while preparing unrelated extension package-boundary artifacts before linting these files (plugin-sdk boundary root shims / rolldown TypeError: Cannot convert undefined or null to object). The scoped wrapper lint above still ran against the changed files.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XS labels Jul 1, 2026
@clawsweeper

clawsweeper Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 1, 2026, 10:59 AM ET / 14:59 UTC.

Summary
The PR changes tool description truncation to use the existing UTF-16-safe helper and adds regression tests for compact and verbose emoji-boundary truncation.

PR surface: Source +1, Tests +32. Total +33 across 2 files.

Reproducibility: yes. from source: current main uses raw UTF-16 slice immediately before appending ellipses in both compact and verbose tool-summary paths, so an emoji can be cut between surrogate halves. I did not run a live current-main repro in this read-only review.

Review metrics: none identified.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Next step before merge

  • No ClawSweeper repair lane is needed because the submitted PR already contains the focused code and regression-test change.

Security
Cleared: No dependency, lockfile, workflow, permission, secret-handling, or third-party code-execution surface is changed.

Review details

Best possible solution:

Land the focused shared-helper fix after normal maintainer review on the exact head, keeping all existing truncation, whitespace, and word-boundary semantics except the surrogate-safe boundary.

Do we have a high-confidence way to reproduce the issue?

Yes from source: current main uses raw UTF-16 slice immediately before appending ellipses in both compact and verbose tool-summary paths, so an emoji can be cut between surrogate halves. I did not run a live current-main repro in this read-only review.

Is this the best way to solve the issue?

Yes. Reusing the existing shared truncateUtf16Safe helper at the two raw-slice sites is the narrowest maintainable fix and avoids duplicating Unicode boundary logic.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against f35fbc898ccf.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body now includes after-fix terminal output exercising the affected formatter path with emoji-boundary descriptions and showing no replacement character or dangling surrogate.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🦞 diamond lobster.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body now includes after-fix terminal output exercising the affected formatter path with emoji-boundary descriptions and showing no replacement character or dangling surrogate.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P3: This is a focused user-visible formatting bug in tool summary text with limited blast radius and no broken core workflow.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body now includes after-fix terminal output exercising the affected formatter path with emoji-boundary descriptions and showing no replacement character or dangling surrogate.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body now includes after-fix terminal output exercising the affected formatter path with emoji-boundary descriptions and showing no replacement character or dangling surrogate.
Evidence reviewed

PR surface:

Source +1, Tests +32. Total +33 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 3 2 +1
Tests 1 32 0 +32
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 35 2 +33

What I checked:

  • Current compact truncation can split surrogates: Current main still truncates compact tool summaries with raw value.slice(0, maxLen - 3) immediately before appending ellipses. (src/agents/tool-description-summary.ts:17, f35fbc898ccf)
  • Current verbose truncation can split surrogates: Current main still truncates /tools verbose descriptions with raw normalized.slice(0, maxLen - 3). (src/agents/tool-description-summary.ts:139, f35fbc898ccf)
  • Patch uses existing safe helper: The PR head imports truncateUtf16Safe and uses it at both raw truncation sites while leaving the whitespace and word-boundary decisions unchanged. (src/agents/tool-description-summary.ts:18, 85572835ae46)
  • Safe helper contract: truncateUtf16Safe delegates to sliceUtf16Safe, whose boundary logic backs off when an end index would leave a dangling surrogate half. (src/shared/utf16-slice.ts:46, f35fbc898ccf)
  • Affected caller surfaces: The shared helper feeds effective tool inventory, Gateway tools.catalog, and /tools verbose, so fixing the shared helper is not a one-sided caller patch. (src/gateway/server-methods/tools-catalog.ts:137, f35fbc898ccf)
  • Real behavior proof updated: The PR body now includes copied terminal output that runs the product formatter path for /tools verbose and the catalog description projection with emoji placed at truncation boundaries, reporting no replacement character and no dangling surrogate. (85572835ae46)

Likely related people:

  • Takhoffman: Merged PR feat: add /tools runtime availability view #54088 added the /tools runtime availability view and introduced src/agents/tool-description-summary.ts with the raw truncation behavior. (role: introduced behavior; confidence: high; commits: 9c7823350bf6; files: src/agents/tool-description-summary.ts, src/auto-reply/status.ts, src/gateway/server-methods/tools-catalog.ts)
  • Bartok9: Merged PR fix(agents): truncate tool-display detail on code-point boundaries #96958 added the dependency-free UTF-16-safe slice helper for the same surrogate-boundary truncation class in tool display code. (role: adjacent invariant contributor; confidence: medium; commits: e09b9dfc1ba9; files: src/shared/utf16-slice.ts, src/agents/tool-display-common.ts)
  • GautamKumarOffical: Merged PR fix: correct tautological uppercase check in tool description summarizer #93753 recently fixed another bug in the same tool description summarizer. (role: recent area contributor; confidence: medium; commits: 01d3812ea24c; files: src/agents/tool-description-summary.ts)
  • vincentkoc: GitHub PR metadata shows this person merged recent fixes in this exact helper and the adjacent UTF-16 helper path. (role: recent merger and adjacent reviewer signal; confidence: medium; commits: 01d3812ea24c, e09b9dfc1ba9; files: src/agents/tool-description-summary.ts, src/shared/utf16-slice.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P3 Low-priority cleanup, docs, polish, ergonomics, or speculative work. labels Jul 1, 2026
@ZengWen-DT

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@ZengWen-DT

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-run

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 1, 2026
@vincentkoc
vincentkoc merged commit d9d6108 into openclaw:main Jul 1, 2026
170 of 181 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 2, 2026
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 3, 2026
sheyanmin pushed a commit to sheyanmin/openclaw that referenced this pull request Jul 8, 2026
Monkey-wusky added a commit to Monkey-wusky/openclaw that referenced this pull request Jul 14, 2026
…cript truncation UTF-16 safe

normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.

Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.

Follows the pattern established by openclaw#98644, openclaw#102470, openclaw#101728 and the broader
UTF-16 safety sweep already applied across the codebase.
Monkey-wusky added a commit to Monkey-wusky/openclaw that referenced this pull request Jul 14, 2026
…cript truncation UTF-16 safe

normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.

Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.

Follows the pattern established by openclaw#98644, openclaw#102470, openclaw#101728 and the broader
UTF-16 safety sweep already applied across the codebase.
Monkey-wusky added a commit to Monkey-wusky/openclaw that referenced this pull request Jul 14, 2026
…cript truncation UTF-16 safe

normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.

Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.

Follows the pattern established by openclaw#98644, openclaw#102470, openclaw#101728 and the broader
UTF-16 safety sweep already applied across the codebase.
Monkey-wusky added a commit to Monkey-wusky/openclaw that referenced this pull request Jul 16, 2026
…cript truncation UTF-16 safe

normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.

Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.

Follows the pattern established by openclaw#98644, openclaw#102470, openclaw#101728 and the broader
UTF-16 safety sweep already applied across the codebase.
steipete added a commit that referenced this pull request Jul 16, 2026
…cript truncation UTF-16 safe (#106370)

* fix(agents,skills): keep session-identity and experience-review transcript truncation UTF-16 safe

normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.

Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.

Follows the pattern established by #98644, #102470, #101728 and the broader
UTF-16 safety sweep already applied across the codebase.

* chore: re-trigger CI after PR body update

* test(agents,skills): construct transcripts exceeding 60k chars, verify old slices dangle

Both experience-review fixtures now exceed EXPERIENCE_REVIEW_MAX_TRANSCRIPT_CHARS
so the truncation branch actually fires. Pre-condition assertions prove the old raw
.slice(0,6000) / .slice(tailStart) produce isolated surrogates; the production
functions (formatSkillExperienceReviewTranscript, formatCliOutputError) do not.

cli-output test also adds pre-condition check showing normalizeCliContextValue
with raw .slice(0,200) would split a surrogate pair.

* test: tighten UTF-16 truncation coverage

Co-authored-by: 毛宇豪0668001457 <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
wangmiao0668000666 pushed a commit to wangmiao0668000666/openclaw that referenced this pull request Jul 16, 2026
…cript truncation UTF-16 safe (openclaw#106370)

* fix(agents,skills): keep session-identity and experience-review transcript truncation UTF-16 safe

normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.

Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.

Follows the pattern established by openclaw#98644, openclaw#102470, openclaw#101728 and the broader
UTF-16 safety sweep already applied across the codebase.

* chore: re-trigger CI after PR body update

* test(agents,skills): construct transcripts exceeding 60k chars, verify old slices dangle

Both experience-review fixtures now exceed EXPERIENCE_REVIEW_MAX_TRANSCRIPT_CHARS
so the truncation branch actually fires. Pre-condition assertions prove the old raw
.slice(0,6000) / .slice(tailStart) produce isolated surrogates; the production
functions (formatSkillExperienceReviewTranscript, formatCliOutputError) do not.

cli-output test also adds pre-condition check showing normalizeCliContextValue
with raw .slice(0,200) would split a surrogate pair.

* test: tighten UTF-16 truncation coverage

Co-authored-by: 毛宇豪0668001457 <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 17, 2026
…cript truncation UTF-16 safe (openclaw#106370)

* fix(agents,skills): keep session-identity and experience-review transcript truncation UTF-16 safe

normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.

Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.

Follows the pattern established by openclaw#98644, openclaw#102470, openclaw#101728 and the broader
UTF-16 safety sweep already applied across the codebase.

* chore: re-trigger CI after PR body update

* test(agents,skills): construct transcripts exceeding 60k chars, verify old slices dangle

Both experience-review fixtures now exceed EXPERIENCE_REVIEW_MAX_TRANSCRIPT_CHARS
so the truncation branch actually fires. Pre-condition assertions prove the old raw
.slice(0,6000) / .slice(tailStart) produce isolated surrogates; the production
functions (formatSkillExperienceReviewTranscript, formatCliOutputError) do not.

cli-output test also adds pre-condition check showing normalizeCliContextValue
with raw .slice(0,200) would split a surrogate pair.

* test: tighten UTF-16 truncation coverage

Co-authored-by: 毛宇豪0668001457 <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling P3 Low-priority cleanup, docs, polish, ergonomics, or speculative work. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants