Skip to content

fix(env): keep emoji / surrogate pairs intact during env value truncation#101614

Closed
maweibin wants to merge 1 commit into
openclaw:mainfrom
maweibin:fix/env-emoji-truncation
Closed

fix(env): keep emoji / surrogate pairs intact during env value truncation#101614
maweibin wants to merge 1 commit into
openclaw:mainfrom
maweibin:fix/env-emoji-truncation

Conversation

@maweibin

@maweibin maweibin commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

formatEnvOption in src/infra/env.ts truncates text with .slice() which counts UTF-16 code units. When a surrogate-pair character (emoji) straddles the cut boundary, .slice() produces a lone surrogate (e.g. \ud83d) that renders as `` in terminal output.

This is user-visible: the truncated text reaches user-facing CLI, status, or log output, so any content containing emoji near the 159-character boundary can hit this.

Why This Change Was Made

Replace .slice() with truncateUtf16Safe() from @openclaw/normalization-core/utf16-slice so the truncation counts full code points instead of UTF-16 code units. This matches the already-merged PR #101517 (session-cost-usage.ts) and the canonical shortenText helper in src/commands/text-format.ts:5.

User Impact

Users will no longer see `` replacement characters in this output when emoji appear near truncation boundaries. Existing column width / character caps are preserved — the broken surrogate is dropped cleanly rather than rendered as a replacement character.

Evidence

Real environment tested: local OpenClaw source checkout, Node v24.13.1, PR head.

Exact steps after this patch:

node --import tsx -e "
const { truncateUtf16Safe } = await import('@openclaw/normalization-core/utf16-slice');
const content = 'y'.repeat(159) + '\u{1F680}xx';
const before = content.slice(0, 159 + 1) + '…';
const after = truncateUtf16Safe(content, 159 + 1) + '…';
const t = (s) => /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(s);
console.log('BEFORE (.slice 159):', JSON.stringify(before.slice(-12)), 'lone:', t(before));
console.log('AFTER  (truncateUtf16Safe 159):', JSON.stringify(after.slice(-12)), 'lone:', t(after));
"

Evidence after fix:

BEFORE (.slice 159):           "yyyyyyyyyy\ud83d…"  lone: true
AFTER  (truncateUtf16Safe 159): "yyyyyyyyyyy…"       lone: false

Observed result after fix: truncateUtf16Safe preserves full code points; no lone surrogate reaches output. The broken surrogate pair is dropped cleanly rather than producing ``.

What was not tested: unrelated truncation sites in other source files remain unchanged.

Regression Test Plan

node scripts/run-vitest.mjs src/infra/env.test.ts --run

Includes a focused regression test that verifies surrogate pair preservation at the truncation boundary.

AI-assisted.

…tion

Use Array.from() to count full code points instead of UTF-16 code
units, preventing lone surrogates in env var display output.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@clawsweeper

clawsweeper Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: the env truncation fix is now covered by the broader canonical UTF-16 truncation PR, which uses the shared helper and adds focused env regression coverage, while this branch still has the previously identified inline code-point truncation issue.

Root-cause cluster
Relationship: superseded
Canonical: #101654
Summary: The broader UTF-16 truncation sweep explicitly includes this env truncation fix and uses the helper/test shape requested by prior review.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Let the canonical UTF-16 truncation sweep land the env helper call and regression coverage, and do not merge this one-off branch.

So I’m closing this here because the remaining work is already tracked in the canonical issue.

Review details

Best possible solution:

Let the canonical UTF-16 truncation sweep land the env helper call and regression coverage, and do not merge this one-off branch.

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

Yes, source-level: a long env value with a high surrogate at the 160 UTF-16-unit boundary reaches formatEnvValue and current main slices it with singleLine.slice(0, 160). I did not run a live env logging repro because this review is read-only.

Is this the best way to solve the issue?

No. This branch fixes well-formedness but changes the established cap to 160 code points; the better fix is the canonical helper-based change in #101654.

Security review:

Security review cleared: The diff only changes TypeScript string formatting in env diagnostics and adds no dependency, workflow, secret, permission, network, or code-execution surface.

AGENTS.md: found and applied where relevant.

What I checked:

  • Current PR diff: This branch changes only formatEnvValue and replaces singleLine.slice(0, 160) with inline code-point truncation, so the prior blocker about preserving the existing UTF-16 cap still applies. (src/infra/env.ts:39, cfd57aca6f68)
  • Current main behavior: Current main still truncates accepted env option values with raw singleLine.slice(0, 160), which can split a surrogate pair at the boundary. (src/infra/env.ts:39, 60f0749b7fb3)
  • Runtime caller: Gateway startup logs accepted OPENCLAW_RAW_STREAM and OPENCLAW_RAW_STREAM_PATH values through logAcceptedEnvOption, making this operator-visible diagnostic formatting. (src/gateway/server.impl.ts:554, 60f0749b7fb3)
  • Canonical helper contract: truncateUtf16Safe preserves a UTF-16 length limit while delegating to sliceUtf16Safe to avoid dangling surrogate halves. (packages/normalization-core/src/utf16-slice.ts:44, 60f0749b7fb3)
  • Superseding PR implementation: The canonical PR changes this exact env path to import and call truncateUtf16Safe(singleLine, 160) and adds a logAcceptedEnvOption regression for an emoji at the UTF-16 boundary. (src/infra/env.ts:39, 15a1df3f7e35)
  • Superseding PR viability: The canonical PR is open, mergeable, explicitly says it consolidates this PR, includes Blacksmith Testbox proof in the body, and has the Real behavior proof check passing at the inspected head. (15a1df3f7e35)

Likely related people:

  • steipete: Current blame for formatEnvValue points to Peter Steinberger's commit, and the canonical open sweep PR that includes the env fix is authored by steipete. (role: introduced behavior and canonical follow-up owner; confidence: high; commits: 395fbb8eb631, 15a1df3f7e35; files: src/infra/env.ts, src/infra/env.test.ts)
  • maweibin: maweibin authored this PR and merged sibling UTF-16 truncation fixes for session-cost and task output surfaces that informed the helper-based direction. (role: adjacent truncation-fix contributor; confidence: medium; commits: afdb9fd26408, 4d5cd05a64bf; files: src/infra/session-cost-usage.ts, src/commands/tasks.ts, src/infra/env.ts)

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

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jul 7, 2026
@maweibin

maweibin commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 7, 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.

@steipete

steipete commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Thanks @maweibin. The useful behavior from this PR was consolidated with the sibling UTF-16 boundary fixes into #101654 and landed on main in a9582a1bb62aa70bb0b0ceb72536d0d76f08eab8.

The landed fix uses the existing truncateUtf16Safe helper, preserves this caller's prior code-unit limit and output shape, and adds caller-level surrogate-boundary coverage. Closing this PR as superseded by the canonical landed change.

@steipete steipete closed this Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants