Skip to content

fix(cron): truncate failure alert error text on UTF-16 boundary#97298

Merged
vincentkoc merged 4 commits into
openclaw:mainfrom
zenglingbiao:fix/utf16-safe-cron-failure-alerts
Jun 28, 2026
Merged

fix(cron): truncate failure alert error text on UTF-16 boundary#97298
vincentkoc merged 4 commits into
openclaw:mainfrom
zenglingbiao:fix/utf16-safe-cron-failure-alerts

Conversation

@zenglingbiao

Copy link
Copy Markdown
Contributor

Summary

Replace String.prototype.slice(0, 200) with truncateUtf16Safe() in cron emitFailureAlert to prevent broken Unicode from surrogate-pair splitting when error messages contain emoji or non-BMP characters.

What Problem This Solves

When emitFailureAlert in src/cron/service/failure-alerts.ts:116 truncates an error message for a cron failure notification, it uses .slice(0, 200) which can split a UTF-16 surrogate pair in half. The resulting alert text routes through chat channels (as noted by the code comment: "Keep alert bodies compact because they may route through chat channels with notification previews") — where users would see garbled characters.

Code evidence: The current code does (params.error?.trim() || "unknown reason").slice(0, 200), operating on UTF-16 code units. Characters outside BMP (emoji, rare CJK, etc.) are encoded as two code units; slicing at an arbitrary index can cut between them.

Root Cause

  • Introduced by: commit 9d31cbbd6a (2026-05-31, refactor: split cron service timer helpers)
  • Violated invariant: String.prototype.slice(n) assumes all characters are single-code-unit BMP characters. Provider error messages can legitimately contain non-BMP characters.
  • Trigger condition: A cron job failure whose error message contains an emoji at the 200-character truncation boundary. The alert is then delivered to a chat channel with a broken character.

Why This Fix

User Impact

  • Affected: Cron failure alerts delivered to chat channels (Telegram, Slack, Discord, etc.)
  • Before: Alert text could end with if the error contained an emoji near the 200-char boundary
  • After: Alert text is always truncated on a valid code-point boundary

Evidence

Terminal proof script demonstrating the bug pattern at the 200-code-unit boundary (matching the exact .slice(0, 200) in emitFailureAlert):

Before (.slice(0, 200) on a 201-code-unit error with emoji at boundary):

  • Truncated length: 200 code units
  • FAIL: Dangling surrogate detected — the emoji surrogate pair was split in half

After (truncateUtf16Safe at the same boundary):

  • Truncated length: 199 code units (safe boundary)
  • PASS: No dangling surrogate

Real Behavior Proof

Before (on main)

$ node --import tsx proof.mjs
=== PROOF: UTF-16 safe truncation in cron failure alerts ===

Input length: 201 UTF-16 code units
Input ends with emoji: true

=== BEFORE (main, .slice(0, 200)) ===
Truncated length: 200
FAIL: Dangling surrogate detected — truncation split the emoji surrogate pair.
Cron failure alerts sent to chat channels would contain garbled characters.

=== AFTER (fix, truncateUtf16Safe) ===
Truncated length: 199
PASS: No dangling surrogate — truncation is UTF-16 safe.

After (with fix)

$ node --import tsx proof.mjs
=== PROOF: UTF-16 safe truncation in cron failure alerts ===

Input length: 201 UTF-16 code units
Input ends with emoji: true

=== BEFORE (main, .slice(0, 200)) ===
Truncated length: 200
FAIL: Dangling surrogate detected — truncation split the emoji surrogate pair.

=== AFTER (fix, truncateUtf16Safe) ===
Truncated length: 199
PASS: No dangling surrogate — truncation is UTF-16 safe.

Tests and Validation

  • pnpm check passed
  • TypeScript compilation verified
  • Manual proof script confirms the fix prevents surrogate-pair splitting

@clawsweeper

clawsweeper Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 27, 2026, 2:58 PM ET / 18:58 UTC.

Summary
The PR replaces cron failure-alert raw error slicing with truncateUtf16Safe and adds a CronService regression test for an emoji at the 200-code-unit boundary.

PR surface: Source +1, Tests +60. Total +61 across 2 files.

Reproducibility: yes. Source inspection shows current main slices the trimmed cron alert error at 200 UTF-16 code units, and an error string with an emoji straddling that boundary reaches the alert text path.

Review metrics: none identified.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
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.

Rank-up moves:

  • none.

Mantis proof suggestion
A Telegram Desktop recording would materially demonstrate the user-visible alert rendering path beyond the unit-level string proof. A maintainer can ask Mantis to capture proof by posting this exact PR comment:

@openclaw-mantis telegram desktop proof: trigger a cron failure alert with an emoji at the 200-code-unit boundary and verify Telegram shows no replacement character.

Next step before merge

  • [P2] No repair lane is needed because the branch is a focused implementation PR and no actionable automated defect was found.

Security
Cleared: The diff reuses an existing local string helper and adds a cron unit test, with no dependency, workflow, permission, secret, package-resolution, or code-execution changes.

Review details

Best possible solution:

Land the narrow helper reuse and regression test after normal maintainer review and required checks remain green.

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

Yes. Source inspection shows current main slices the trimmed cron alert error at 200 UTF-16 code units, and an error string with an emoji straddling that boundary reaches the alert text path.

Is this the best way to solve the issue?

Yes. The alert text is assembled in emitFailureAlert, and reusing the existing dependency-free helper there fixes every cron failure-alert delivery path without adding new policy, configuration, or duplicate truncation logic.

AGENTS.md: found and applied where relevant.

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

Label changes

Label justifications:

  • P2: This is a normal-priority user-visible cron alert text corruption fix with a limited, well-tested blast radius.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit 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 copied terminal output showing raw slicing creates a dangling surrogate at the 200-code-unit boundary and the helper avoids it; live Telegram proof would be useful but is not a contributor blocker here.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes copied terminal output showing raw slicing creates a dangling surrogate at the 200-code-unit boundary and the helper avoids it; live Telegram proof would be useful but is not a contributor blocker here.
  • mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. Cron failure-alert text can be visible in Telegram announcements, so a short Telegram Desktop proof could demonstrate that the boundary case no longer renders a replacement character.
Evidence reviewed

PR surface:

Source +1, Tests +60. Total +61 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 2 1 +1
Tests 1 60 0 +60
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 62 1 +61

What I checked:

Likely related people:

  • 0xbrak: Authored the merged configurable cron failure-alert feature that created the affected user-visible alert surface. (role: introduced behavior; confidence: high; commits: 4637b90c070f; files: src/cron/service/timer.ts, src/cron/service.failure-alert.test.ts, src/gateway/server-cron.ts)
  • steipete: Split cron failure-alert logic into the current owner module while preserving the raw truncation behavior. (role: recent area contributor; confidence: high; commits: 9d31cbbd6ad8; files: src/cron/service/failure-alerts.ts, src/cron/service/timer.ts)
  • Bartok: Introduced the shared UTF-16-safe slicing helper reused by this PR. (role: adjacent helper contributor; confidence: medium; commits: e09b9dfc1ba9; files: src/shared/utf16-slice.ts, src/utils.ts)
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 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. P2 Normal backlog priority with limited blast radius. labels Jun 27, 2026
Verify that emitFailureAlert does not produce dangling surrogates when
truncating a cron failure error message with a non-BMP character (emoji)
straddling the 200-code-unit truncation boundary.
@zenglingbiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. and removed 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. labels Jun 27, 2026
@clawsweeper
clawsweeper Bot temporarily deployed to qa-live-shared June 27, 2026 18:34 Inactive
The dangling surrogate check loop stopped at length-1, missing a
high surrogate at the final position. Extend to i < alertText.length
so charCodeAt(i+1) returns NaN for the last char, correctly failing
the high-surrogate pair assertion.
@zenglingbiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

- Use template literal instead of string concatenation
- Add braces to type guard if-statement
@zenglingbiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added 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: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jun 27, 2026
@openclaw-mantis

Copy link
Copy Markdown
Contributor

Mantis Telegram Desktop Proof

Summary: Mantis captured Telegram Desktop before/after GIFs for the cron failure alert text.

Main screenshot This PR screenshot
Baseline native Telegram Desktop screenshot Candidate native Telegram Desktop screenshot
Main This PR
Baseline native Telegram Desktop proof GIF Candidate native Telegram Desktop proof GIF

Motion-trimmed clips:

Raw QA files: https://artifacts.openclaw.ai/mantis/telegram-desktop/pr-97298/run-28298061543-1/index.json

@vincentkoc
vincentkoc merged commit 686a287 into openclaw:main Jun 28, 2026
113 of 119 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 29, 2026
…claw#97298)

* fix(cron): truncate failure alert error text on UTF-16 boundary

* test(cron): add regression test for UTF-16 safe failure alert truncation

Verify that emitFailureAlert does not produce dangling surrogates when
truncating a cron failure error message with a non-BMP character (emoji)
straddling the 200-code-unit truncation boundary.

* fix(test): cover last code unit in surrogate scan

The dangling surrogate check loop stopped at length-1, missing a
high surrogate at the final position. Extend to i < alertText.length
so charCodeAt(i+1) returns NaN for the last char, correctly failing
the high-surrogate pair assertion.

* fix(test): address lint issues in regression test

- Use template literal instead of string concatenation
- Add braces to type guard if-statement
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
…claw#97298)

* fix(cron): truncate failure alert error text on UTF-16 boundary

* test(cron): add regression test for UTF-16 safe failure alert truncation

Verify that emitFailureAlert does not produce dangling surrogates when
truncating a cron failure error message with a non-BMP character (emoji)
straddling the 200-code-unit truncation boundary.

* fix(test): cover last code unit in surrogate scan

The dangling surrogate check loop stopped at length-1, missing a
high surrogate at the final position. Extend to i < alertText.length
so charCodeAt(i+1) returns NaN for the last char, correctly failing
the high-surrogate pair assertion.

* fix(test): address lint issues in regression test

- Use template literal instead of string concatenation
- Add braces to type guard if-statement
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 1, 2026
…claw#97298)

* fix(cron): truncate failure alert error text on UTF-16 boundary

* test(cron): add regression test for UTF-16 safe failure alert truncation

Verify that emitFailureAlert does not produce dangling surrogates when
truncating a cron failure error message with a non-BMP character (emoji)
straddling the 200-code-unit truncation boundary.

* fix(test): cover last code unit in surrogate scan

The dangling surrogate check loop stopped at length-1, missing a
high surrogate at the final position. Extend to i < alertText.length
so charCodeAt(i+1) returns NaN for the last char, correctly failing
the high-surrogate pair assertion.

* fix(test): address lint issues in regression test

- Use template literal instead of string concatenation
- Add braces to type guard if-statement
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 3, 2026
…claw#97298)

* fix(cron): truncate failure alert error text on UTF-16 boundary

* test(cron): add regression test for UTF-16 safe failure alert truncation

Verify that emitFailureAlert does not produce dangling surrogates when
truncating a cron failure error message with a non-BMP character (emoji)
straddling the 200-code-unit truncation boundary.

* fix(test): cover last code unit in surrogate scan

The dangling surrogate check loop stopped at length-1, missing a
high surrogate at the final position. Extend to i < alertText.length
so charCodeAt(i+1) returns NaN for the last char, correctly failing
the high-surrogate pair assertion.

* fix(test): address lint issues in regression test

- Use template literal instead of string concatenation
- Add braces to type guard if-statement
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mantis: telegram-visible-proof Mantis should capture Telegram visible proof. 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. 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.

2 participants