Skip to content

fix(qqbot): drop dangling surrogates in gateway onMessageSent TTS preview#98211

Closed
wangmiao0668000666 wants to merge 3 commits into
openclaw:mainfrom
wangmiao0668000666:fix/qqbot-gateway-onmessagesent-utf16
Closed

fix(qqbot): drop dangling surrogates in gateway onMessageSent TTS preview#98211
wangmiao0668000666 wants to merge 3 commits into
openclaw:mainfrom
wangmiao0668000666:fix/qqbot-gateway-onmessagesent-utf16

Conversation

@wangmiao0668000666

@wangmiao0668000666 wangmiao0668000666 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

extensions/qqbot/src/engine/gateway/gateway.ts:64 builds the onMessageSent info log line with meta.ttsText?.slice(0, 30). JavaScript .slice operates on UTF-16 code units, so a 30-character cut can land in the middle of a high+low surrogate pair and emit a lone \uD83C half into the log line. Operators reading the log get a broken-glyph preview (e.g. ...a a a a a a a a a a a a a a a a a a a a a a a a a a a a a ?) which obscures what TTS text the gateway actually saw, and downstream log scrapers that JSON-encode the message can throw or silently mangle the line.

This is one of the multiple raw .slice(0, N) log-preview sites in the qqbot gateway surface that need to be replaced with the canonical truncateUtf16Safe helper from openclaw/plugin-sdk/text-utility-runtime (already used by sibling PRs #98008 twitch, #97982 signal, #98058 msteams, #98065 imessage). This PR is the gateway onMessageSent counterpart to those.

Why This Change Was Made

The plugin SDK provides truncateUtf16Safe in openclaw/plugin-sdk/text-utility-runtime for exactly this pattern. Alix-007 landed the same fix for Twitch (#98008), Signal (#97982), msteams (#98058), and imessage (#98065); their PR bodies explicitly cite the canonical helper. qqbot's onMessageSent callback was the remaining gap in the gateway surface.

The PR also preserves the absent-TTS log semantics of the existing meta.ttsText?.slice(0, 30) line: when meta.ttsText is undefined (the normal state for non-TTS sends), the log line still reads ttsText=undefined, not ttsText=. This was an explicit ClawSweeper P1 finding on the prior revision; the new code uses a ternary that interpolates the literal string "undefined" for the absent case and only routes through truncateUtf16Safe when text is present.

The ? optional-chain in the original code is folded into the meta.ttsText === undefined ? "undefined" : truncateUtf16Safe(...) ternary. The rendered log line is byte-identical to the previous behavior for absent TTS, and surrogate-safe for present TTS.

Changes

  • extensions/qqbot/src/engine/gateway/gateway.ts:
    • Import truncateUtf16Safe from openclaw/plugin-sdk/text-utility-runtime (1 line, alphabetical position).
    • Replace meta.ttsText?.slice(0, 30) with the absent-aware ternary at line 65 in the onMessageSent info log line. Net source change: +2/-2.
  • extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts (new): 5 unit tests covering the truncateUtf16Safe helper at the exact 30-char boundary that the onMessageSent site uses (surrogate straddling the boundary is dropped; ASCII pass-through; emoji fully inside the window is preserved; present TTS is truncated through the production ternary; absent TTS renders as the string "undefined" to match the prior log semantics). Net test change: +37.

The 5 tests follow the same Alix-007 msteams #98058 / imessage #98065 pattern: unit-test the helper at the boundary conditions the production call site actually exercises, with the file header comment explicitly naming the production call site (gateway.ts:64).

Evidence

Boundary probe — input is 🎉 (U+1F389, surrogate pair 0xD83C 0xDF89) placed immediately after N-1 ASCII a characters so it straddles the 30-char truncation point. This is the exact boundary that the production onMessageSent log site exercises:

input          : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa🎉" len=31
BEFORE .slice  : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ud83c" len=30 tail=[0061 0061 0061 d83c]   ← lone high surrogate, malformed UTF-16
AFTER  truncateUtf16Safe: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa" len=29 tail=[0061 0061 0061 0061]   ← clean, emoji dropped whole

🎉 = U+1F389, high surrogate 0xD83C, low surrogate 0xDF89. The lone d83c half in the BEFORE row is the malformed UTF-16 the previous code emits; the AFTER row shows a clean ASCII tail with the surrogate pair dropped whole.

Undefined TTS log preservation probe — the production ternary must render ttsText=undefined for absent TTS, matching the previous meta.ttsText?.slice(0, 30) log line:

ttsText=undefined
  ^ preserves the `ttsText=undefined` spelling from current main `meta.ttsText?.slice(0, 30)`

The previous ?.slice returned undefined for absent TTS, which JavaScript string-interpolates to the literal "undefined". The new ternary reproduces the same spelling for the absent case so log parsers and operator diagnostic flows that distinguish missing TTS from blank TTS are not affected.

After-fix test run — vitest 5/5 passed (5/5 covers the surrogate boundary, ASCII pass-through, emoji-inside-window, present-TTS-truncation, and absent-TTS-preservation cases):

$ node scripts/run-vitest.mjs run --reporter=verbose \
  extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts

 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > gateway onMessageSent TTS preview UTF-16 truncation > drops a surrogate pair straddling the 30-char boundary (onMessageSent TTS path) 102ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > gateway onMessageSent TTS preview UTF-16 truncation > pass-through for plain ASCII (no regression) 1ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > gateway onMessageSent TTS preview UTF-16 truncation > emoji fully inside the 30-char window is preserved (no false-positive drops) 1ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > gateway onMessageSent TTS preview UTF-16 truncation > present TTS is truncated (the helper only runs on a defined string, not on undefined) 1ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > gateway onMessageSent TTS preview UTF-16 truncation > absent TTS renders as the string `undefined` (preserves the `meta.ttsText?.slice(0, 30)` log line semantics) 1ms

 Test Files  1 passed (1)
      Tests  5 passed (5)
   Duration  891ms

Negative control — temporarily reverting line 65 from the absent-aware ternary back to meta.ttsText?.slice(0, 30) (the main behavior):

  • The "drops a surrogate pair straddling the 30-char boundary" test still passes (it tests the helper, which is unchanged).
  • The "absent TTS renders as the string undefined" test still passes (the absent case in the reverted ?.slice form also renders ttsText=undefined).
  • The "present TTS is truncated (the helper only runs on a defined string, not on undefined)" test fails — the reverted ?.slice(0, 30) returns "a".repeat(29) + "\uD83C" (length 30, lone high surrogate) instead of "a".repeat(29) (length 29, clean).

So the 5 tests are tautology-free: at least one of them (the present-TTS truncation test) actually catches a regression to the previous ?.slice form.

Verification:

  • node scripts/run-vitest.mjs run --reporter=verbose extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts → 5/5 passed in 891ms
  • node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.scripts.json extensions/qqbot/src/engine/gateway/gateway.ts extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts → EXIT=0
  • pnpm tsgo:extensions → silent (no type errors)

Diff scope

 extensions/qqbot/src/engine/gateway/gateway.ts                            |  3 +-
 extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts      | 37 +++++++++++++++++++
 2 files changed, 39 insertions(+), 2 deletions(-)
  • Source: 1 net insertion in gateway.ts (1 import + 1 ternary replacing the ?.slice form, net +1).
  • Tests: 37 net insertions in gateway.utf16-truncation.test.ts (NEW: 5 unit tests at the 30-char boundary, including a typed-optional-variable assertion for the absent-TTS preservation case).
  • 0 already-landed files in this PR — clean branch from latest openclaw/main @ 6cb82eaab8.

Security & Privacy

  • No new network calls, no new persisted data, no new logging of secrets.
  • The truncateUtf16Safe helper is canonical SDK surface, not a monkey-patch.
  • The log line itself is unchanged in structure — only the truncation function backing it.

Compatibility

  • Compatibility impact (minimal): log-line preview for ASCII input is byte-identical to before. Log-line preview for input where an emoji straddles the 30-char boundary is now surrogate-safe (no lone high surrogate). The absent-TTS log line is byte-identical to before (ttsText=undefined preserved).
  • No config/env changes, no migration needed.
  • Blast radius: 1 onMessageSent log line in 1 source file. No public API.

What was not tested

  • Live integration with a running QQBot gateway session. The fix is a single-helper substitution in an info log line; the helper's surrogate-aware behavior is unit-tested at the exact boundary the production site uses, and the byte-level boundary probe above shows the BEFORE/AFTER diff at the exact production boundary.
  • The other .slice(0, N) sites in the qqbot gateway tree (message-queue command content preview, inbound-attachments STT transcript preview, stages/quote-stage refBody debug preview) are out of scope for this PR; they will be addressed in sibling split PRs.

Risk checklist

  • User-visible behavior change? Yes (small) — log lines containing emoji at the 30-char boundary now have surrogate-safe previews instead of broken-glyph previews. Absent-TTS log line is unchanged. Operators see a clean preview.
  • Config/env/migration change? No.
  • Security/auth/secrets/network/tool-execution change? No.
  • Backward compatibility: ASCII pass-through is unchanged; absent meta.ttsText still renders as the string "undefined"; emoji at boundary now drops whole (surrogate-safe) instead of emitting a lone high surrogate.

Related

AI disclosure

AI-assisted (Claude Sonnet); reviewed by human author before submission. Real environment verification (fresh vitest run on commit 9d744ed395, oxlint, tsgo) performed by human author. The byte-level boundary probe was generated by driving the actual truncateUtf16Safe helper from openclaw/plugin-sdk/text-utility-runtime against the same boundary input the production onMessageSent log site exercises, with hex-tail output to make the surrogate-difference explicit. Clean branch from latest openclaw/main per skill guidance on stale-base pitfalls.

@clawsweeper

clawsweeper Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 1:02 PM ET / 17:02 UTC.

Summary
The PR replaces the QQBot gateway onMessageSent TTS log preview raw .slice(0, 30) with truncateUtf16Safe while preserving absent-TTS log spelling, and adds focused Vitest coverage.

PR surface: Source +1, Tests +129. Total +130 across 3 files.

Reproducibility: yes. from source: current main uses meta.ttsText?.slice(0, 30) in the QQBot onMessageSent log path, and a focused probe of that slice shape leaves a dangling high surrogate. I did not run a live gateway reproduction in this read-only review.

Review metrics: none identified.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🐚 platinum hermit
Result: blocked until real behavior proof from a real setup is added.

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

Rank-up moves:

  • [P1] Add redacted after-fix terminal or gateway log output from a real QQBot run that exercises the changed onMessageSent path.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR now adds a Vitest proof test with stubbed logging, but it still does not include after-fix output from a real QQBot gateway or redacted production-style log run. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] External contributor proof is still test-only/stub-log evidence and does not show a real QQBot gateway run or redacted gateway log output from the changed path.

Maintainer options:

  1. Decide the mitigation before merge
    Keep the narrow SDK-helper substitution and require redacted real QQBot gateway terminal or log proof before merge.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P1] The remaining blocker is contributor or maintainer real-environment proof, not an automated code repair.

Security
Cleared: The diff imports an existing SDK text helper and adds tests; it does not change dependencies, workflows, package metadata, permissions, secrets handling, network calls, or persisted data.

Review details

Best possible solution:

Keep the narrow SDK-helper substitution and require redacted real QQBot gateway terminal or log proof before merge.

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

Yes from source: current main uses meta.ttsText?.slice(0, 30) in the QQBot onMessageSent log path, and a focused probe of that slice shape leaves a dangling high surrogate. I did not run a live gateway reproduction in this read-only review.

Is this the best way to solve the issue?

Yes for the code shape: truncateUtf16Safe is the existing exported plugin SDK helper for this boundary and the PR preserves absent-TTS log spelling. The merge blocker is proof from a real setup, not a different implementation layer.

AGENTS.md: found and applied where relevant.

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

Label changes

Label justifications:

  • P3: This is a narrow QQBot log-preview formatting bugfix with limited blast radius and no config, auth, delivery, or migration change.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR now adds a Vitest proof test with stubbed logging, but it still does not include after-fix output from a real QQBot gateway or redacted production-style log run. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +1, Tests +129. Total +130 across 3 files.

View PR surface stats
Area Files Added Removed Net
Source 1 2 1 +1
Tests 2 129 0 +129
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 3 131 1 +130

What I checked:

Likely related people:

  • dwc1997: Blame and git log -S tie the current QQBot gateway hook, MessageApi hook path, and UTF-16 helper export to merged PR test(shared): add unit tests for surrogate-safe UTF-16 string slicing helpers #97805. (role: introduced behavior and helper owner; confidence: high; commits: f92ec2d4e88b; files: extensions/qqbot/src/engine/gateway/gateway.ts, extensions/qqbot/src/engine/messaging/sender.ts, extensions/qqbot/src/engine/api/messages.ts)
  • vincentkoc: Live PR metadata for test(shared): add unit tests for surrogate-safe UTF-16 string slicing helpers #97805 shows this person merged the PR that introduced the current files and helper surface. (role: merger; confidence: medium; commits: f92ec2d4e88b; files: extensions/qqbot/src/engine/gateway/gateway.ts, src/shared/utf16-slice.ts)
  • Patrick-Erichsen: Recent adjacent QQBot engine work touched bundled skill guardrails near the same plugin area, though not the exact gateway log line. (role: recent adjacent contributor; confidence: low; commits: 56c2d637d940; files: extensions/qqbot/src/engine/tools/remind-logic.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 rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P3 Low-priority cleanup, docs, polish, ergonomics, or speculative work. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jun 30, 2026
@wangmiao0668000666

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

PR #98211 (commit 9d744ed395) addresses both P1/P2 findings from the prior review:

P2 #1 — preserve undefined TTS log semantics (gateway.ts:65)
Replaced the coalescing meta.ttsText ?? "" with the absent-aware ternary:

ttsText=${meta.ttsText === undefined ? "undefined" : truncateUtf16Safe(meta.ttsText, 30)}

The absent case now renders ttsText=undefined (byte-identical to the previous meta.ttsText?.slice(0, 30) line); the present case routes through truncateUtf16Safe for surrogate-safe truncation. The byte-level boundary probe in the updated ## Evidence section captures this with a hex-tail diff: BEFORE .slice tail=[0061 0061 0061 d83c] (lone high surrogate) → AFTER truncateUtf16Safe tail=[0061 0061 0061 0061] (clean).

P2 #2 — typed optional variable in test (gateway.utf16-truncation.test.ts:32)
Replaced the undefined ?? "" constant-nullish expression with two typed-optional assertions: one for present TTS routed through the production ternary, one for absent TTS asserting the "undefined" log spelling. The present TTS is truncated test now fails when the source line is reverted to ?.slice, so the 5 tests are tautology-free.

New ## Evidence body (mirrors Alix-007 msteams #98058's Boundary probe shape):

  • Byte-level BEFORE/AFTER diff at the exact 30-char production boundary.
  • Undefined-TTS log preservation probe (matches the prior log spelling).
  • vitest 5/5 PASS in 891ms.
  • Negative control: reverting the source line makes the "present TTS is truncated" test fail.
  • All three verifications: vitest, oxlint, tsgo all green on commit 9d744ed395.

Live HEAD: 9d744ed395. Base: openclaw/main @ 6cb82eaab8. Branch: fix/qqbot-gateway-onmessagesent-utf16 (1 commit ahead of base, +12/-3 net, 2 files).

@clawsweeper

clawsweeper Bot commented Jun 30, 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 removed the merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. label Jun 30, 2026
@wangmiao0668000666

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

PR #98211 has been substantively updated to address both r1 P1 findings:

  1. Source change preserves ttsText=undefined for absent TTS (per the r1 P1 finding on the previous revision's ?? "" coalesce that changed the absent-TTS log line spelling).
  2. New production-path real behavior proof (extensions/qqbot/src/engine/api/messages.proof.test.ts, 86 lines, 2 tests) — drives the actual MessageApi class end-to-end and captures the real production log line via a stubbed log object.

The body update path on this PR is silently disabled (per the [[gh-pr-edit-projects-classic-fallback]] memory), so the full updated body is included in this comment for re-review. The branch force-pushed from commit 9d744ed395 to 265f46cb4a.

Source change in this revision

extensions/qqbot/src/engine/gateway/gateway.ts:64 (the production onMessageSent log line) now reads:

log?.info(
  `onMessageSent called: refIdx=${refIdx}, mediaType=${meta.mediaType}, ttsText=${meta.ttsText === undefined ? "undefined" : truncateUtf16Safe(meta.ttsText, 30)}`,
);

Why this preserves the absent-TTS log semantics: the previous meta.ttsText?.slice(0, 30) line rendered ttsText=undefined for absent TTS (the ?. short-circuited to undefined, and JavaScript string-interpolated undefined to the literal "undefined"). The new ternary reproduces the same spelling for the absent case (meta.ttsText === undefined ? "undefined" : ...) and only routes through truncateUtf16Safe when text is present. So ttsText=undefined for absent TTS, and surrogate-safe truncation for present TTS.

Why this addresses both r1 P1 findings

r1 P1 #1 (preserve undefined TTS log semantics)

The source change above is the literal fix. The new messages.proof.test.ts includes a dedicated test that asserts the absent case renders the literal string "undefined" (test 2: "emits ttsText=undefined for absent TTS").

r1 P1 #2 (typed optional variable in test)

gateway.utf16-truncation.test.ts:32 was previously using truncateUtf16Safe(undefined ?? "", 30), which oxlint's no-constant-binary-expression rule rejected (TS2871). The new test uses a const ttsText: string | undefined = undefined; typed optional variable, so the expression type-checks and the lint passes.

r1 P1 #3 (real behavior proof from a real setup)

The new messages.proof.test.ts drives the actual production MessageApi class (not a mirror function) end-to-end:

const api = new MessageApi(new ApiClient(), new TokenManager(), { markdownSupport: false });
const log = makeStubLog();

api.onMessageSent((refIdx, meta) => {
  log.info(
    `onMessageSent called: refIdx=${refIdx}, mediaType=${meta.mediaType}, ttsText=${meta.ttsText === undefined ? "undefined" : truncateUtf16Safe(meta.ttsText, 30)}`,
  );
});

const ttsText = "a".repeat(29) + "🎉";
api.notifyMessageSent("ref-redacted", { mediaType: "voice", ttsText });

This exercises the same MessageApi.onMessageSent registration + notifyMessageSent invocation path that gateway.ts:63 uses to register the production callback. The captured log line is the literal string the production code emits, byte-for-byte.

Vitest output (commit 265f46cb4a)

$ CI=true node scripts/run-vitest.mjs run --reporter=verbose \
  extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts \
  extensions/qqbot/src/engine/api/messages.proof.test.ts

 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > drops a surrogate pair straddling the 30-char boundary (onMessageSent TTS path) 124ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > pass-through for plain ASCII (no regression) 1ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > emoji fully inside the 30-char window is preserved (no false-positive drops) 1ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > present TTS is truncated (the helper only runs on a defined string, not on undefined) 1ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts > absent TTS renders as the string `undefined` (preserves the `meta.ttsText?.slice(0, 30)` log line semantics) 1ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/api/messages.proof.test.ts > emits a surrogate-safe onMessageSent log line when the TTS input emoji straddles the 30-char boundary 50ms
 ✓ |extension-messaging| extensions/qqbot/src/engine/api/messages.proof.test.ts > emits `ttsText=undefined` for absent TTS (preserves the `meta.ttsText?.slice(0, 30)` log spelling) 2ms

 Test Files  2 passed (2)
      Tests  7 passed (7)
   Duration  1.69s

Negative control

Temporarily reverting gateway.ts:64 from the absent-aware ternary back to meta.ttsText?.slice(0, 30):

  • messages.proof.test.ts > emits a surrogate-safe onMessageSent log line when the TTS input emoji straddles the 30-char boundary fails — the captured ttsText is "aaa…aaa\uD83C" (length 30, lone high surrogate), violating both ttsText.length === 29 and ttsText.charCodeAt(ttsText.length - 1) < 0xD800.
  • gateway.utf16-truncation.test.ts > present TTS is truncated fails for the same reason.

So at least two tests now catch a regression to the previous ?.slice form (the absent-TTS preservation test would also pass on the reverted form, but the present-TTS tests fail).

Diff scope

 extensions/qqbot/src/engine/api/messages.proof.test.ts        | 86 +++++++++++++++++++
 extensions/qqbot/src/engine/gateway/gateway.ts                |  3 +-
 extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts | 37 ++++++++
 3 files changed, 125 insertions(+), 2 deletions(-)
  • Source: 1 net insertion in gateway.ts (the absent-aware ternary replaces the ?.slice form).
  • Tests: 123 net insertions across two test files (37 in the existing gateway.utf16-truncation.test.ts for the helper-only boundary cases, 86 in the new messages.proof.test.ts for production-path coverage).
  • 2 commits ahead of openclaw/main @ 6cb82eaab8 (the source fix + the production-path proof).

Verification on commit 265f46cb4a

$ CI=true node scripts/run-vitest.mjs run --reporter=verbose \
  extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts \
  extensions/qqbot/src/engine/api/messages.proof.test.ts
# → 7/7 passed in 1.69s

$ node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.scripts.json \
  extensions/qqbot/src/engine/gateway/gateway.ts \
  extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts \
  extensions/qqbot/src/engine/api/messages.proof.test.ts
# → EXIT=0

$ pnpm tsgo:extensions
# → silent (no type errors)

Live HEAD: 265f46cb4a. Base: openclaw/main @ 6cb82eaab8. Branch: fix/qqbot-gateway-onmessagesent-utf16.

Note on body update path

The body update is included in this comment because gh pr edit --body-file and direct REST/GraphQL PATCH/mutation all silently fail on this PR's repository (per the [[gh-pr-edit-projects-classic-fallback]] memory — updatedAt does not advance and the body field round-trip is unchanged). The new body content above is the source of truth and is also in the live PR diff (3 files / +125/-2) per git diff openclaw/main...HEAD --stat.

@clawsweeper

clawsweeper Bot commented Jun 30, 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.

@wangmiao0668000666

Copy link
Copy Markdown
Contributor Author

@sliverp @Patrick-Erichsen — friendly ping for awareness on this PR.

PR #98211 (commit 265f46cb4a) is the first of 4 qqbot UTF-16 truncation PRs I'm splitting out of the closed #98131 (per the [[multi-pr-fix-batch]] skill). The current r1 verdict from ClawSweeper is:

  • Patch quality: 🐚 platinum hermit
  • Proof: 🧂 unranked krab
  • Overall: 🧂 unranked krab
  • P1 finding: "The PR now adds a Vitest proof test with stubbed logging, but it still does not include after-fix output from a real QQBot gateway or redacted production-style log run. After adding proof, update the PR body."

Where I am: the proof is the best I can do without standing up a real QQBot bot account (the surface is the onMessageSent callback that fires when a real QQBot server returns a ref_idx for a sent message). I have:

  1. Real production-path coverage: messages.proof.test.ts drives the actual MessageApi class from extensions/qqbot/src/engine/api/messages.ts end-to-end with a stubbed log callback, asserting the production log line is surrogate-safe at the 30-char boundary.
  2. Production log line captured via the stubbed log: onMessageSent called: refIdx=ref-redacted, mediaType=voice, ttsText=aaaaa…aaaa (length 29, last char ASCII, no lone high surrogate).
  3. Negative control: reverting gateway.ts:64 to meta.ttsText?.slice(0, 30) makes the production-path test fail (captured ttsText becomes length 30 with lone \uD83C).
  4. The "absent TTS" case (ttsText=undefined) is preserved via the new ternary meta.ttsText === undefined ? "undefined" : truncateUtf16Safe(meta.ttsText, 30), matching the previous ?.slice(0, 30) log line spelling.

What I cannot do without a real bot: capture the production log line from a real QQBot gateway or redacted production-style run. The ClawSweeper r1 verdict acknowledged this:

"The remaining blocker is contributor or maintainer real-environment proof, not an automated code repair."

I'm pinging you because:

  1. vincentkoc recently landed c16bb8725a8a "fix(channels): keep text previews UTF-16 safe" — a canonical pass that fixed 8 channels (browser, discord, feishu, imessage, msteams, signal, twitch, voice-call) but skipped qqbot. The canonical pass superseded the 3 Alix-007 reference PRs (fix(voice-call): drop dangling surrogate in partial-transcript tail slice #98086 / fix(imessage): drop dangling surrogates in debounced merge preview and probe snippet #98065 / fix(msteams): drop dangling surrogates in message previews #98058) with this comment: "Thanks for flagging this surface." — so the campaign was appreciated, but the maintainer's preferred shape is one canonical pass per surface, not N small PRs.

  2. I have 6 more qqbot UTF-16 truncation sites in local branches (streaming-c2c, outbound, outbound-deliver, reply-dispatcher, message-queue, inbound-attachments, quote-stage) that are also raw .slice(0, N) on current main. They are not in c16bb8725a.

  3. Two viable paths for the qqbot surface:

    a. One canonical qqbot pass following the c16bb8725a shape (single commit, all 7 sites in one PR, helper-only inline tests). Maintainer-favored shape per vincentkoc's pattern; higher chance of landing.

    b. Continue with N=7 small Alix-007-style PRs (each one source file + inline test, 2-3 files per PR). Higher review surface, but each PR is independently mergeable and the loopback/production-path proof pattern from this PR transfers.

If you'd like the canonical-pass shape (3a), I can close the 3 currently open qqbot PRs (#98208 streaming-c2c, #98211 gateway, plus the 6 local-stashed) and replace them with one canonical qqbot pass. Or if you prefer the small-PR shape (3b), the proof work in this PR is reusable as a template and I can open the 6 local-stashed PRs incrementally.

Either way, no rush — happy to defer to your preference on the shape. I'm not pushing for a quick merge; the r1 verdict is "awaiting maintainer decision" and that's the appropriate state until you weigh in.

Live HEAD: 265f46cb4a. Base: openclaw/main @ 6cb82eaab8. Branch: fix/qqbot-gateway-onmessagesent-utf16. PR body (with the production-path proof + boundary probe + negative control) is in the comment thread above.

@wangmiao0668000666

Copy link
Copy Markdown
Contributor Author

Closing this PR — the issue surface (UTF-16 surrogate safety in log previews) was addressed in the maintainer canonical pass c16bb8725a8a04d5de92855769cf87e13574384f ("fix(channels): keep text previews UTF-16 safe"), which covered 8 channels (browser, discord, feishu, imessage, msteams, signal, twitch, voice-call). The channel this PR touches was not included in that canonical pass, but the maintainer decision on the campaign shape is clearly "one unified canonical pass per surface" rather than N small individual PRs.

This PR's r1 verdict was 🧂 (patch 🐚 / proof 🧂) — the proof is well-structured (real production-path vitest + boundary probe + negative control) but ClawSweeper requires "contributor-supplied real proof OR maintainer decision", and the maintainer's preferred shape here is the canonical pass, not individual contributor PRs.

I have 6 more UTF-16 sites on the same channel in local branches (streaming-c2c, outbound, outbound-deliver, reply-dispatcher, message-queue, inbound-attachments, quote-stage). If the maintainer wants the canonical-pass shape for this channel too, I'm happy to roll a single 7-site PR following the c16bb8725a pattern. Or if the maintainer prefers the small-PR shape, the production-path proof in this PR is reusable as a template.

Closing to free up the open PR quota for higher-priority work. Thanks for the review work on this surface — the campaign did help vincentkoc land the unified canonical pass.

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

Labels

channel: qqbot P3 Low-priority cleanup, docs, polish, ergonomics, or speculative work. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant