fix(media): keep input_file text truncation UTF-16 safe#102634
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 9, 2026, 5:57 AM ET / 09:57 UTC. Summary PR surface: Source +1, Tests +37. Total +38 across 2 files. Reproducibility: yes. from source inspection: current main slices decoded input_file text at the 60,000 code-unit cap before wrapping it into the model prompt, so a boundary after a high surrogate can create a dangling surrogate. The PR body also includes a before/after boundary proof and an after-fix HTTP regression path. Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Next step before merge
Security Review detailsBest possible solution: Use the shared UTF-16-safe truncation helper at the input_file extraction owner, with focused HTTP coverage of the model-visible prompt boundary. Do we have a high-confidence way to reproduce the issue? Yes from source inspection: current main slices decoded input_file text at the 60,000 code-unit cap before wrapping it into the model prompt, so a boundary after a high surrogate can create a dangling surrogate. The PR body also includes a before/after boundary proof and an after-fix HTTP regression path. Is this the best way to solve the issue? Yes. The fix is at the extraction owner before both direct text and PDF-extracted text are rendered into AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against c86fe21cada0. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +1, Tests +37. Total +38 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
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
|
2365ce0 to
cae64db
Compare
|
Maintainer fixup and land-ready proof for exact head The final regression now proves the real Proof:
No external provider key was used: the defect and asserted invariant terminate at the model-facing |
|
Merged via squash.
|
Summary
/v1/responsesinput_filetext usedtext.slice(0, maxChars), so an emoji at the default60_000character cap could be split into a dangling UTF-16 surrogate in the model-visible file context.truncateUtf16Safefrom@openclaw/normalization-core/utf16-sliceinsrc/media/input-files.ts::clampText.input_filecontent through/v1/responses.<file>wrapper.What Problem This Solves
Oversized text extracted from an OpenResponses
input_filecan land exactly inside an emoji surrogate pair. The currentclampText()implementation slices by UTF-16 code unit, so a payload like"a".repeat(59_999) + "😀tail"is truncated to a file-context string ending in the lone high surrogate\uD83D. That malformed model-visible text can then be forwarded inextraSystemPromptinstead of a valid UTF-16 boundary-aligned prefix.User Impact
Users attaching large local text files through
/v1/responsescan get corrupted file context at the truncation boundary when the boundary falls inside emoji or another astral Unicode character. After this patch, the same oversized file is still capped, but the capped text never ends with a split surrogate pair.Origin / follow-up
fix(codex): use truncateUtf16Safe for tool transcript output truncation).src/media/input-files.ts::clampTextstill usedtext.slice(0, maxChars)on the canonical/v1/responsesinput_filepath.truncateUtf16Safehelper rather than adding a local surrogate check.Competition / linked PR analysis
src/media/input-files.ts.input_filetext clamp before the file text is wrapped and passed intoagentCommand({ extraSystemPrompt })./v1/responsesrequest, runs real base64input_fileextraction, and asserts on the model-visibleextraSystemPrompt.Related scan:
Merge risk
/v1/responsesHTTP boundary.as neverin the test only satisfies the existing typedagentCommandmock return shape and does not construct the malformed runtime state; the malformed state comes from a real HTTP request payload and realinput_fileextraction. No fallback, broad catch, default shim, or unrelated cleanup was added.src/gateway/openresponses-http.test.ts.src/media/input-files.ts::clampTextusedtext.slice(0, maxChars), which can split UTF-16 surrogate pairs because JavaScript string indices are code-unit based.truncateUtf16Safe, whose source-of-truth implementation backs up before a split surrogate pair instead of masking the corrupted value downstream.src/media/input-files.ts::extractFileContentFromSourceowns model-visible text extraction forinput_file; this changes the text before downstream prompt wrapping and beforeagentCommand, not a mirrored or persisted projection.input_filewhose default cap lands between the two UTF-16 code units of😀./v1/responsesserver path and checks the final model-visible prompt payload, while the production change is a one-line replacement of the unsafe clamp.Real behavior proof
/v1/responsesinput_filetext truncation could split emoji at the defaultmaxCharsboundary and forward a dangling surrogate inextraSystemPrompt./v1/responses→ OpenResponses HTTP input parsing insrc/gateway/openresponses-http.ts→input_filesource passed toextractFileContentFromSource()→src/media/input-files.ts::clampText→renderFileContextBlock({ content: wrapUntrustedFileContent(rawText) })→agentCommand({ extraSystemPrompt })./v1/responsesrequest parsing, real base64input_fileextraction, and assertion onagentCommandmodel prompt input.truncateUtf16Safefrom@openclaw/normalization-core/utf16-slice. Provider constraint check is N/A because this is not a provider range/parser change./media/vdb/code/ai/aispace/openclaw-worktrees/followup-102478after rebase onto currentorigin/main, Node/Vitest throughpnpm -C "$TASK_WORKTREE" exec vitest.extraSystemPromptcontains<file name="emoji-boundary.txt">, does not include the split emoji, and does not match the lone high-surrogate regex/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/u.agentCommandboundary, which is the source boundary for this local prompt-construction bug. URLinput_filefetching and PDF image extraction were not separately exercised because they converge on the sameclampTexthelper after extraction.Evidence
Focused command output from the rebased fix worktree:
Additional focused extraction guard suite:
Before/after repro for the exact boundary:
Review findings addressed
Regression Test Plan
Risk / Compatibility
Low risk. The request schema, endpoint behavior, MIME and byte validation, URL fetch guards, PDF extraction, and config defaults are unchanged. The only observable difference is for oversized extracted file text whose cap would otherwise land inside a surrogate pair; that output now drops the incomplete pair instead of forwarding a lone surrogate.
What was not changed
Root Cause
src/media/input-files.ts::clampTextusedtext.slice(0, maxChars)and could cut between UTF-16 surrogate halves.truncateUtf16Safeenforces the boundary invariant at the onlyinput_filetext clamp before downstream prompt construction.extractFileContentFromSourceis the owner of normalizedinput_filetext before it is wrapped for the model. The patch changes the source-of-truth text before feedback/model input, not only a mirror/projection/persistence record.