pi-mono/packages/coding-agent/src
Petr Houška 9e870f6f00
fix(coding-agent): do not split surrogate pairs when truncating text (#11757)
Several places truncate text by UTF-16 code units with `slice()`. When
the cut lands between the two halves of a surrogate pair (an emoji or
other character outside the Basic Multilingual Plane), the result keeps
a lone high or low surrogate. That is invalid UTF-16.

Example: a grep match line with 499 ASCII characters followed by `😀` is
cut at 500 units, so the output ends in `\uD83D... [truncated]`.

The bundled providers strip lone surrogates before sending
(`sanitizeSurrogates` in pi-ai), so the usual effect is a dropped
character. But the broken text still reaches the session file, RPC and
JSON clients (strict UTF-8 encoders such as Rust's serde_json reject
lone surrogates), extensions, and custom providers.

This adds `sliceHead(text, n)` and `sliceTail(text, n)` to
`core/tools/truncate.ts`. They behave like `slice(0, n)` and `slice(-n)`
but return one unit fewer when the cut would split a pair, and are used
where truncated text reaches the model:
- `truncateLine` (grep match lines), the case reported in #7121
- codemode `truncateOutput` head and tail, and its argument and error
previews
- compaction `truncateForSummary` (tool results sent to the summarizer)
- MCP server summaries in the system prompt and stderr tails in
connection errors

The cost is two `charCodeAt` checks per truncation, and nothing on the
untruncated path.

The surrogate check follows the fix in #7122 by @IKEASven69, which
@cristinaponcela reviewed in a follow-up branch. That PR was closed
before merging.

Not changed: TUI-only previews (renderers, visual truncation) and
line-based slicing. pi-private's pi-durable codemode `output.ts` has the
same head/tail slicing and needs a separate change there.

**Testing**
- New `test/truncate-surrogates.test.ts`: an emoji at the cut in
`truncateLine` is dropped whole, an emoji ending exactly at the limit is
kept, `sliceHead`/`sliceTail` never leave a lone surrogate for any
length, and they match `slice` on text without surrogates.
- `test/tools.test.ts`, compaction, MCP extension, and codemode tests
pass.
- `npm run check` passes.

fixes #7121

This PR is AI-generated.
2026-11-04 20:59:46 +01:00
..
bun feat(coding-agent): codemode and MCP 2026-09-29 09:12:50 +02:00
cli fix(coding-agent): load user keybindings in pi config (#11788) 2026-11-04 09:59:49 +01:00
core fix(coding-agent): do not split surrogate pairs when truncating text (#11757) 2026-11-04 20:59:46 +01:00
experimental feat(coding-agent): remove the experimental server stack and the client, protocol, and server packages 2026-10-12 10:41:59 +00:00
extensions fix(coding-agent): do not split surrogate pairs when truncating text (#11757) 2026-11-04 20:59:46 +01:00
modes fix(coding-agent): preserve pasted text during editor restoration (#10146) 2026-11-04 11:20:58 +01:00
utils fix(coding-agent): ignore prerelease suffix of last seen changelog version (#11753) 2026-11-03 21:59:05 +01:00
cli.ts fix(coding-agent): keep remote harness dependencies development-only 2026-09-05 12:45:04 +02:00
config.ts feat(coding-agent): add PI_SELF_UPDATE_COMMAND and selfUpdateCommand for pi update (#11629) 2026-10-30 22:30:39 +01:00
index.ts feat(coding-agent): let Markdown transformers opt into more surfaces (#11756) 2026-11-03 23:10:47 +01:00
main.ts fix(coding-agent): dispose session runtime after startup benchmark (#11736) 2026-11-03 22:11:40 +01:00
migrations.ts fix(coding-agent): update repository links to "earendil-works/pi" (#9278) 2026-09-07 14:09:39 +02:00
package-manager-cli.ts fix(coding-agent): only report packages as up to date when pi update checked them (#11711) 2026-11-02 21:49:07 +01:00
rpc-entry.ts feat(coding-agent): set AI_AGENT environment variable (#7493) 2026-08-03 13:45:32 +02:00