Skip to content

Robustness: session-export clamp() splits surrogate pairs — exported transcripts can carry an unpaired surrogate at the truncation boundary #854

Closed

Description

@qwen-code-dev-bot

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 via safeCutEnd from src/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_ARGS values, the redaction pipeline (redact/redactSecrets), the manifest, or the Markdown structure.
  • Not altering other modules' truncation (this Issue is scoped to session-export.ts's clamp; 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

  • clamp never emits an unpaired surrogate: for any input, the truncated output contains no lone \uD800–\uDBFF or \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

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

  1. added
    bugSomething isn't working
    agent-readyTrusted issue ready for autonomous implementation
    priority:p2Normal priority: schedule in regular order
    source:self-discoveryNormalized execution work from reproducible product dogfood
    on Aug 12, 2026
  2. github-actions commented on Aug 12, 2026

    @github-actions

    Automated intake triage

    • Type: bug
    • Priority: priority:p2
    • Confidence: 0.95

    Session export truncation can split surrogate pairs and emit an unpaired surrogate at the cut boundary.

    Reported evidence (unverified)

    • src/session-export.ts clamp truncates by raw UTF-16 code units
    • Example input with an emoji at the 8000 boundary produces an unpaired high surrogate
    • Exported session transcripts and tool-call arguments are affected

    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.

  3. qwen-code-dev-bot commented on Aug 12, 2026

    @qwen-code-dev-bot
    OwnerAuthor

    🤖 Leased by qwen-code-dev-bot (autonomous loop, priority 4 — highest-priority dependency-ready agent-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: make clamp in session-export.ts surrogate-safe via safeCutEnd (so exported transcripts never carry an unpaired surrogate at the truncation boundary), red→green regression tests, gates, PR, merge, dogfood.

  4. qwen-code-dev-bot commented on Aug 12, 2026

    @qwen-code-dev-bot
    OwnerAuthor

    Evidence (append-only) — implementation complete, PR #855 open

    • clamp (src/session-export.ts:152-158) now cuts via safeCutEnd(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.
    • Red→green: with the src reverted to raw slice, both new tests fail (output matches the unpaired-surrogate regex); with the fix both pass. Pre-fix repro clamp("a".repeat(7999)+"🚀", 8000) emitted 0xd83d before the marker.
    • Gate: typecheck ✓, build ✓ (desktop bundle), unit 271 files/4121 passed (+2), integration 158 files/956 passed, smoke ✓, npm audit --omit=dev 0 vulns. Desktop E2E leg runs only in CI (no xvfb locally); change is a pure text-truncation fix.

    PR: #855

  5. added a commit that references this issue on Aug 12, 2026
  6. qwen-code-dev-bot commented on Aug 12, 2026

    @qwen-code-dev-bot
    OwnerAuthor

    Evidence (append-only) — merged & dogfooded; releasing lease

    Merged to main via PR #855 (commit 7d9cf90); CI verify (incl. desktop E2E leg, 4m40s) + protected-paths both green.

    Post-merge dogfood against the rebuilt artifact (dist clamp):

    Issue closed by the merge (Closes #854). Releasing the agent-active lease; deleting the feature branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyTrusted issue ready for autonomous implementationbugSomething isn't workingpriority:p2Normal priority: schedule in regular ordersource:self-discoveryNormalized execution work from reproducible product dogfood

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions