Repository navigation
Robustness: invocation one-line display collapse() splits surrogate pairs — provider/tool/MCP Result/Stdout/Stderr can carry an unpaired surrogate #856
Description
sourceType
source:self-discovery — undirected maintainer review pass. Same surrogate-safety defect class as the merged truncation series (#812, #842, #848, #854), in three sibling sites that series did not cover.
sourceLinkOrEvidence
Concrete evidence on main @ 7d9cf90. Three identical local collapse helpers truncate the captured output for the one-line invocation text view by raw UTF-16 code units:
src/provider-invocation.ts:512-516— used at line 505 for theResult:line.src/tool-invocation.ts:420-424— used at lines 410/413 for theStdout:/Stderr:lines.src/mcp-invocation.ts:620-624— used at line 613 for theResult:line.
Each is:
const MAX_DISPLAY_OUTPUT = 240;
function collapse(text: string): string {
const oneLine = text.replace(/\s+/g, " ").trim();
if (oneLine.length <= MAX_DISPLAY_OUTPUT) return oneLine;
return `${oneLine.slice(0, MAX_DISPLAY_OUTPUT)} …[+${oneLine.length - MAX_DISPLAY_OUTPUT} chars]`;
}The captured-output byte caps were already made surrogate-safe (#832/#834/#836/#848), so the text entering collapse is clean — but collapse then re-truncates at 240 code units with String.prototype.slice, which can orphan half of a surrogate pair and emit an unpaired surrogate into the user-visible invocation report.
Minimal reproduction (mirrors collapse exactly):
collapse("a".repeat(239) + "🚀 tail")
Observed: the 🚀 (code units 239–240) straddles the 240 cut; the output contains an unpaired high surrogate (0xd83d) immediately before the …[+N chars] marker.
problemStatement
The provider/tool/MCP invocation text views render captured output through a local collapse that bounds the one-line display with String.prototype.slice(0, 240). Because slice indexes UTF-16 code units, an astral character (emoji, etc.) straddling the 240 boundary is split, leaving an unpaired surrogate in the rendered Result:/Stdout:/Stderr: line. The upstream byte caps are surrogate-safe, but this separate display truncation reintroduces the defect. Same class as #854 (session-export clamp), in three more sites.
userValue
The one-line invocation summaries (provider Result:, tool Stdout:/Stderr:, MCP Result:) never show a broken/orphaned surrogate at the truncation boundary. An emoji or astral character straddling the 240-char display cap is dropped whole, so invocation reports render cleanly.
scope
- Make all three
collapsehelpers surrogate-safe by cutting viasafeCutEndfromsrc/text-cut.ts(import it in each ofprovider-invocation.ts,tool-invocation.ts,mcp-invocation.ts). - Compute the truncation count from the actual safe cut point (e.g.
oneLine.length - cut) so the…[+N chars]marker stays accurate when the safe cut backs off one code unit. - Preserve existing behavior for all non-surrogate-boundary inputs (same collapsed prefix, same marker format, same whitespace collapsing).
nonGoals
- Not changing
MAX_DISPLAY_OUTPUT(240), the whitespace-collapse/trim behavior, or the surrounding invocation report format. - Not touching the already-safe captured-output byte caps (Robustness: make the tool-invocation output-size cap surrogate-safe (spawnCommandRunner) #832/Robustness: account subprocess output caps in UTF-8 bytes, not UTF-16 code units (hook/tool/MCP) #834/Robustness: reassemble multi-byte UTF-8 split across chunks in the hook/tool/MCP output caps #836/Robustness: make the provider-invocation output cap UTF-8-byte-accurate and surrogate-safe (capOutput) #848).
- Not altering other truncation sites (this Issue is scoped to the three invocation-display
collapsehelpers). - Desktop, Dynamic Workflow, or governance surfaces.
acceptanceCriteria
- None of the three
collapseoutputs can contain an unpaired surrogate (no lone\uD800–\uDBFFor\uDC00–\uDFFF). - An astral character straddling the 240 cap is dropped whole (e.g.
"a".repeat(239)+"🚀 tail"collapses with no surrogate before the marker). - The truncation-count marker reflects the actual safe cut point.
- Non-boundary inputs are byte-for-byte unchanged from current behavior (including whitespace collapsing and in-range pass-through).
- Regression tests cover the surrogate-straddle case plus an in-range and a mid-ASCII-truncation case for the invocation display; existing tests pass.
testPlan
Unit tests (extend the existing provider/tool/mcp invocation suites): drive the invocation text-formatting path with captured output that places an astral character exactly across the 240 display cap, asserting the rendered line has no unpaired surrogate (regex /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/) and the marker is present/accurate; plus an in-range case (no truncation) and an ASCII-overflow case (unchanged prefix/marker). Deterministic string assertions.
dogfoodPlan
Build from the merged branch and run the provider/tool/mcp invocation unit suites; additionally render an invocation report whose captured output carries emoji/astral content across the 240-char cap and confirm no broken glyph at the truncation boundary and the marker still present.
riskAndSecurityNotes
Low risk: display formatting only. Behavior is preserved except the surrogate fix; whitespace collapsing, the byte caps, and the report structure are untouched. No secrets/credentials/network action involved; the display path remains downstream of the redaction/cap pipeline.
duplicateSearchEvidence
gh search issuesforinvocation collapse surrogate,invocation display truncat surrogate,collapse display surrogate,Result Stdout surrogate,invocation one-line surrogate→ no matching Issue/PR.- The merged byte-cap series is scoped to the captured-output caps, not this display collapse: Robustness: make the tool-invocation output-size cap surrogate-safe (spawnCommandRunner) #832 ("tool-invocation output-size cap … (spawnCommandRunner)"), Robustness: make the provider-invocation output cap UTF-8-byte-accurate and surrogate-safe (capOutput) #848 ("provider-invocation output cap … (capOutput)"), Robustness: account subprocess output caps in UTF-8 bytes, not UTF-16 code units (hook/tool/MCP) #834/Robustness: reassemble multi-byte UTF-8 split across chunks in the hook/tool/MCP output caps #836 (byte units / chunk reassembly).
- The three
collapsehelpers currently use raw.slice(0, 240)and none imports atext-cuthelper.
Conclusion: distinct, unfixed, untested surrogate-split sites — not a duplicate.
parentChildRelationship
Standalone source:self-discovery Issue (not a child of any roadmap parent). Complements the merged surrogate-safe truncation series by covering the three invocation-display collapse sites it missed.
dependencyOrder
No blocking dependencies; executable immediately (safeCutEnd already exists in src/text-cut.ts). Single vertical slice: make three identical helpers surrogate-safe plus focused regression tests.
Activity
github-actions commentedon Aug 12, 2026
Automated intake triage
bugpriority:p20.90Invocation one-line display helpers can split surrogate pairs when truncating captured output, producing unpaired surrogates in provider, tool, and MCP result displays.
Reported evidence (unverified)
Repository verification
Pending. Repository verification is still required before this Issue can become agent-ready.
This bounded pass classifies intake only. It cannot execute code, close the Issue, or grant governance authority.
qwen-code-dev-bot commentedon Aug 12, 2026
🤖 Leased by
qwen-code-dev-bot(autonomous loop, priority 4 — highest-priority dependency-readyagent-ready; the higher-priority Desktop/Dynamic-Workflow subtasks are blocked by open governance blockers #74/#106/#133/#142). Branch:issue/856-invocation-collapse. Starting the train: make the three invocation-displaycollapsehelpers (provider/tool/MCP) surrogate-safe viasafeCutEnd, red→green regression tests, gates, PR, merge, dogfood.qwen-code-dev-bot commentedon Aug 12, 2026
Evidence (append-only) — implementation complete, PR #857 open
collapsehelpers (src/provider-invocation.ts:513, src/tool-invocation.ts:421, src/mcp-invocation.ts:621) now cut viasafeCutEnd(oneLine, MAX_DISPLAY_OUTPUT)and count the…[+N chars]marker from the actual safe cut point; an astral char straddling the 240 display cap is dropped whole instead of orphaning an unpaired surrogate.slice, the surrogate-straddle test fails (output matches the unpaired-surrogate regex); with the fix all three new tests pass. Pre-fix reprocollapse("a".repeat(239)+"🚀 tail")emitted0xd83dbefore the marker.npm audit --omit=dev0 vulns. Desktop E2E leg runs only in CI (no xvfb locally); change is a pure text-truncation fix.PR: #857
Merge pull request #857 from qwen-code-dev-bot/issue/856-invocation-c…
qwen-code-dev-bot commentedon Aug 12, 2026
Evidence (append-only) — merged & dogfooded; releasing lease
Merged to main via PR #857 (commit 3bd091b). CI note: the first CI run's desktop-E2E leg failed with a generic "Desktop renderer did not reach expected state" timeout, but the desktop renderer imports only erased
import types (no invocation/collapse/text-cut references), so the change cannot affect it; a re-run of the failed job passed (verify 5m15s), confirming a flaky Electron/Xvfb E2E.Post-merge dogfood against the rebuilt artifact:
safeCutEnd(oneLine, MAX_DISPLAY_OUTPUT)present in dist for all three invocation modules.Issue closed by the merge (
Closes #856). Releasing theagent-activelease; feature branch deleted.