Repository navigation
Robustness: session-export clamp() splits surrogate pairs — exported transcripts can carry an unpaired surrogate at the truncation boundary #854
Description
sourceType
source:self-discovery — undirected maintainer review pass over the repository itself (no diff trigger). Same surrogate-safety defect class as the merged truncation series (#812, #842, #848), in a site that series did not cover.
sourceLinkOrEvidence
Concrete evidence on main @ efdddff. src/session-export.ts:151-153 defines a local clamp that truncates by raw UTF-16 code units:
function clamp(text: string, max: number): string {
if (text.length <= max) return text;
return `${text.slice(0, max)} …[+${text.length - max} chars truncated]`;
}clamp is applied in renderSessionMarkdown to both exported message content (clamp(redact(m.content), MAX_CONTENT), MAX_CONTENT = 8000) and tool-call arguments (clamp(redact(tc.function?.arguments ?? ""), MAX_ARGS), MAX_ARGS = 2000). src/session-export.ts imports no text-cut helper (safeCutEnd/clampMarked), and its git history contains no surrogate/encoding fix.
Minimal reproduction (mirrors clamp exactly):
clamp("a".repeat(7999) + "🚀", 8000)
Observed: the 🚀 (code units 7999–8000) straddles the 8000 cut; the output ends …\ud83d …[+1 chars truncated] — an unpaired high surrogate (0xd83d) immediately before the truncation marker, i.e. a broken character is written into the exported Markdown transcript.
problemStatement
session-export truncates long message content and tool arguments with String.prototype.slice(0, max), which indexes UTF-16 code units. When an astral character (emoji, CJK extension, etc.) straddles the MAX_CONTENT/MAX_ARGS boundary, the cut orphans one half of its surrogate pair, emitting an unpaired surrogate into the exported Markdown transcript and JSON-adjacent rendering. This is the same defect the surrogate-safe truncation series (#812 safeCutEnd, #842 clampMarked, #848 provider output cap) fixed elsewhere; session-export.ts's local clamp was missed.
userValue
Exported session transcripts (--export-session) never contain a broken/orphaned surrogate at a truncation boundary. An emoji or other astral character that straddles the cap is dropped whole (like the rest of the truncation), so shared exports render cleanly instead of showing a replacement/broken glyph.
scope
- Make
clamp(src/session-export.ts) surrogate-safe by cutting viasafeCutEndfromsrc/text-cut.ts(import it). - Compute the truncation count from the actual safe cut point (e.g.
text.length - cut) so the…[+N chars truncated]marker stays accurate when the safe cut backs off one code unit. - Preserve the existing behavior for all non-surrogate-boundary inputs (same truncated prefix, same marker format).
nonGoals
- Not changing
MAX_CONTENT/MAX_ARGSvalues, the redaction pipeline (redact/redactSecrets), the manifest, or the Markdown structure. - Not altering other modules' truncation (this Issue is scoped to
session-export.ts'sclamp; other sites, if any, are separate findings). - Not changing the exported file naming, atomicity, or overwrite/force semantics.
- Desktop, Dynamic Workflow, or governance surfaces.
acceptanceCriteria
-
clampnever emits an unpaired surrogate: for any input, the truncated output contains no lone\uD800–\uDBFFor\uDC00–\uDFFF. - An astral character straddling the cap is dropped whole (e.g.
"a".repeat(7999)+"🚀"clamped at 8000 ends in…[+N chars truncated]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 the current behavior.
- Regression tests cover the surrogate-straddle case plus an in-range and a mid-ASCII-truncation case; existing tests pass.
testPlan
Unit tests in tests/unit/session-export.test.ts (extend the existing suite): drive renderSessionMarkdown/export with a message whose content places an astral character exactly across MAX_CONTENT (and a tool-argument case across MAX_ARGS), asserting the emitted Markdown has no unpaired surrogate (regex /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/) and the truncation marker is present and accurate; plus an in-range case asserting no truncation and an ASCII-overflow case asserting unchanged prefix/marker. Deterministic string assertions.
dogfoodPlan
Build from the merged branch and run the session-export unit suite; additionally export a session containing emoji/astral content long enough to hit MAX_CONTENT and confirm the resulting .session-export.md has no broken glyph at the truncation boundary and still ends with the truncation marker.
riskAndSecurityNotes
Low risk: display/export formatting only. Behavior is preserved except the surrogate fix; redaction (redact/redactSecrets) and the manifest/integrity digest are untouched. No secrets, credentials, or network/external-state action involved. The exported output remains privacy-safe (redaction order unchanged — redact runs before clamp).
duplicateSearchEvidence
gh search issuesforsession-export surrogate,session export clamp,session-export truncat,export surrogate→ no matching Issue/PR.src/session-export.tsimports notext-cuthelper and its git history (2e95e5d, 55f5df2, be507ba, e263163) contains no surrogate/encoding fix.- The merged surrogate-safe series (Robustness: make ellipsis truncation surrogate-safe — never split an emoji/astral character at the cut boundary #812
safeCutEnd, Robustness: surrogate-safe truncation for tui-shell tool-text rendering (transcript, output, hints) #842clampMarked, Robustness: make the provider-invocation output cap UTF-8-byte-accurate and surrogate-safe (capOutput) #848 provider cap) covers other truncation sites, notsession-export.ts'sclamp.
Conclusion: distinct, unfixed, untested surrogate-split site — 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 session-export site it missed.
dependencyOrder
No blocking dependencies; executable immediately (safeCutEnd already exists in src/text-cut.ts). Single vertical slice: make one function surrogate-safe plus focused regression tests.
Activity
github-actions commentedon Aug 12, 2026
Automated intake triage
bugpriority:p20.95Session export truncation can split surrogate pairs and emit an unpaired surrogate at the cut boundary.
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/854-session-export-clamp. Starting the train: makeclampin session-export.ts surrogate-safe viasafeCutEnd(so exported transcripts never carry an unpaired surrogate at the truncation boundary), red→green regression tests, gates, PR, merge, dogfood.qwen-code-dev-bot commentedon Aug 12, 2026
Evidence (append-only) — implementation complete, PR #855 open
clamp(src/session-export.ts:152-158) now cuts viasafeCutEnd(text, max)and counts the truncation marker from the actual safe cut point; an astral character straddling MAX_CONTENT/MAX_ARGS is dropped whole instead of orphaning an unpaired surrogate in the export.slice, both new tests fail (output matches the unpaired-surrogate regex); with the fix both pass. Pre-fix reproclamp("a".repeat(7999)+"🚀", 8000)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: #855
Merge pull request #855 from qwen-code-dev-bot/issue/854-session-expo…
qwen-code-dev-bot commentedon Aug 12, 2026
Evidence (append-only) — merged & dogfooded; releasing lease
Merged to main via PR #855 (commit 7d9cf90); CI
verify(incl. desktop E2E leg, 4m40s) +protected-pathsboth green.Post-merge dogfood against the rebuilt artifact (dist
clamp):safeCutEndpresent indist/session-export.js(3 references).Issue closed by the merge (
Closes #854). Releasing theagent-activelease; deleting the feature branch.