Skip to content

fix(codex): keep CLI session preview text on code-point boundaries#96582

Merged
vincentkoc merged 1 commit into
openclaw:mainfrom
WeeLi-009:fix/codex-session-preview-surrogate
Jun 27, 2026
Merged

fix(codex): keep CLI session preview text on code-point boundaries#96582
vincentkoc merged 1 commit into
openclaw:mainfrom
WeeLi-009:fix/codex-session-preview-surrogate

Conversation

@WeeLi-009

@WeeLi-009 WeeLi-009 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary: The Codex CLI session preview (lastMessage) was truncated with value.slice(0, max-3) on raw UTF-16, tearing an emoji into a lone surrogate in the session-list JSON. This uses truncateUtf16Safe to truncate on a code-point boundary.

What Problem This Solves

Codex CLI session previews in extensions/codex/src/node-cli-sessions.ts truncated text with raw UTF-16 .slice(). When the preview limit landed between the high and low surrogate of an emoji, the displayed lastMessage could contain a lone surrogate.

Trigger input used for proof:

  • 136 ASCII characters
  • followed by 🤖 (U+1F916, two UTF-16 code units)
  • followed by trailing text

The old slice(0, 137) + "..." behavior kept only the high surrogate (0xd83e) before the ellipsis.

Fix

truncateText now reuses the shared truncateUtf16Safe helper from openclaw/plugin-sdk/text-utility-runtime instead of slicing raw UTF-16 code units.

The fix keeps the existing preview width and ellipsis behavior, clamps the safe truncation budget with Math.max(0, max - 3), and preserves the existing escaping/formatting path by only changing the preview truncation boundary.

Evidence

Command run from an isolated proof worktree on fix/codex-session-preview-surrogate:

node --import tsx demo.mts

The demo imports the real createCodexCliSessionNodeHostCommands() entry point from ./extensions/codex/src/node-cli-sessions.ts, writes a temporary Codex history.jsonl, invokes the real codex.cli.sessions.list handler, and compares old raw .slice() behavior with the fixed handler output.

input.length(code units)= 142
input boundary codePoints= 0x61 0x61 0x1f916 0x74 0x61 0x69
BEFORE old slice preview tail= "aaaaaaaa\ud83e..."
BEFORE old slice tail codeUnits= 0x0061 0x0061 0x0061 0x0061 0xd83e 0x002e 0x002e 0x002e
BEFORE lone-surrogate= true last code unit before ellipsis= 0xd83e
AFTER handler preview tail= "aaaaaaaaa..."
AFTER handler preview length= 139
AFTER handler tail codeUnits= 0x0061 0x0061 0x0061 0x0061 0x0061 0x002e 0x002e 0x002e
AFTER lone-surrogate= false
AFTER preview= "aaaaaaaaaaaaaaaaaaaa…[136 ×'a' elided]..."

The old path produces a lone high surrogate at the truncation boundary. The fixed real handler output is well-formed UTF-16 and the lone-surrogate check is false.

Tests

Targeted Vitest coverage was added for both Codex preview sources:

  • history-backed session previews keep UTF-16 code point boundaries
  • session-file-backed previews keep UTF-16 code point boundaries

Validation run:

node scripts/run-vitest.mjs extensions/codex/src/node-cli-sessions.test.ts

Test Files  1 passed (1)
Tests       7 passed (7)

truncateText shortened the cached lastMessage preview with value.slice(0, max - 3), which can cut a surrogate pair in half and emit a lone surrogate into the codex CLI session list JSON. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add regressions for both the history.jsonl and sessions/**/*.jsonl preview paths.
@openclaw-barnacle openclaw-barnacle Bot added extensions: codex size: S triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jun 24, 2026
@clawsweeper

clawsweeper Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 26, 2026, 12:04 AM ET / 04:04 UTC.

Summary
The PR changes Codex CLI session preview truncation to use truncateUtf16Safe and adds regression tests for history-backed and session-file-backed previews.

PR surface: Source +4, Tests +61. Total +65 across 2 files.

Reproducibility: yes. Current main sends both Codex history and rollout session-file preview text through a helper that raw-slices UTF-16, and the PR body demonstrates the boundary case through the real node command handler.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: serialized state: extensions/codex/src/node-cli-sessions.test.ts. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Next step before merge

  • No ClawSweeper repair lane is needed; the branch already contains the focused code change, regression tests, and sufficient terminal proof.

Security
Cleared: The diff only changes a local TypeScript truncation helper call and colocated tests; it does not touch dependencies, workflows, package metadata, secrets, or supply-chain execution paths.

Review details

Best possible solution:

Land or adopt the helper-based fix so Codex CLI session previews are truncated at UTF-16 code point boundaries across both history and rollout sources.

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

Yes. Current main sends both Codex history and rollout session-file preview text through a helper that raw-slices UTF-16, and the PR body demonstrates the boundary case through the real node command handler.

Is this the best way to solve the issue?

Yes. The shared local truncateText helper is the narrowest maintainable fix point; changing each reader separately would duplicate the boundary rule, and changing Codex storage parsing is not needed.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 0fdfc9f65ff4.

Label changes

Label justifications:

  • P2: This is a narrow Codex CLI preview correctness fix with limited blast radius and focused regression coverage.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes terminal proof that imports the real Codex CLI session-list node command, writes temporary Codex history data, invokes the handler, and shows the lone-surrogate case fixed.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes terminal proof that imports the real Codex CLI session-list node command, writes temporary Codex history data, invokes the handler, and shows the lone-surrogate case fixed.
Evidence reviewed

PR surface:

Source +4, Tests +61. Total +65 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 5 1 +4
Tests 1 61 0 +61
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 66 1 +65

What I checked:

  • Current main raw-slices the history-backed preview: Current main reads history.jsonl, passes non-empty text through truncateText(parsed.text.trim(), 140), and truncateText currently uses value.slice(0, max - 3), which can cut a surrogate pair. (extensions/codex/src/node-cli-sessions.ts:374, 0fdfc9f65ff4)
  • Current main raw-slices the session-file preview too: The rollout session-file path also feeds user message text into the same truncateText(messageText, 140) helper, so a one-point helper fix covers both preview inputs. (extensions/codex/src/node-cli-sessions.ts:480, 0fdfc9f65ff4)
  • PR changes the shared local truncation point: The patch imports truncateUtf16Safe from the plugin SDK text runtime and replaces the raw slice inside the local truncateText helper, preserving the existing max-width and ellipsis behavior. (extensions/codex/src/node-cli-sessions.ts:692, 52e2b003e63d)
  • Regression tests cover both preview sources: The diff adds tests for history.jsonl and sessions/**/*.jsonl previews with emoji at the truncation boundary and asserts the resulting lastMessage has no dangling surrogate half. (extensions/codex/src/node-cli-sessions.test.ts:211, 52e2b003e63d)
  • Shared helper contract matches the intended fix: truncateUtf16Safe clamps the requested length and delegates to sliceUtf16Safe, whose end-boundary check backs off when the slice would leave a high surrogate without its low surrogate. (src/utils.ts:100, 0fdfc9f65ff4)
  • Extension import boundary is valid: The scoped extension policy permits extension production code to import from openclaw/plugin-sdk/*, and package.json exports ./plugin-sdk/text-utility-runtime. (extensions/AGENTS.md:27, 0fdfc9f65ff4)

Likely related people:

What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 24, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. label Jun 25, 2026
@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Jun 26, 2026
@vincentkoc
vincentkoc merged commit b5c662f into openclaw:main Jun 27, 2026
188 of 205 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 28, 2026
…penclaw#96582)

truncateText shortened the cached lastMessage preview with value.slice(0, max - 3), which can cut a surrogate pair in half and emit a lone surrogate into the codex CLI session list JSON. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add regressions for both the history.jsonl and sessions/**/*.jsonl preview paths.
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
…penclaw#96582)

truncateText shortened the cached lastMessage preview with value.slice(0, max - 3), which can cut a surrogate pair in half and emit a lone surrogate into the codex CLI session list JSON. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add regressions for both the history.jsonl and sessions/**/*.jsonl preview paths.
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 1, 2026
…penclaw#96582)

truncateText shortened the cached lastMessage preview with value.slice(0, max - 3), which can cut a surrogate pair in half and emit a lone surrogate into the codex CLI session list JSON. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add regressions for both the history.jsonl and sessions/**/*.jsonl preview paths.
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 3, 2026
…penclaw#96582)

truncateText shortened the cached lastMessage preview with value.slice(0, max - 3), which can cut a surrogate pair in half and emit a lone surrogate into the codex CLI session list JSON. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add regressions for both the history.jsonl and sessions/**/*.jsonl preview paths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extensions: codex P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants