fix(qqbot): drop dangling surrogates in gateway onMessageSent TTS preview#98211
fix(qqbot): drop dangling surrogates in gateway onMessageSent TTS preview#98211wangmiao0668000666 wants to merge 3 commits into
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 1:02 PM ET / 17:02 UTC. Summary PR surface: Source +1, Tests +129. Total +130 across 3 files. Reproducibility: yes. from source: current main uses 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:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest 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 Is this the best way to solve the issue? Yes for the code shape: AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against a75431c586ce. Label changesLabel justifications:
Evidence reviewedPR surface: Source +1, Tests +129. Total +130 across 3 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
|
|
@clawsweeper re-review PR #98211 (commit P2 #1 — preserve undefined TTS log semantics ( ttsText=${meta.ttsText === undefined ? "undefined" : truncateUtf16Safe(meta.ttsText, 30)}The absent case now renders P2 #2 — typed optional variable in test ( New
Live HEAD: |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…r gateway TTS preview
|
@clawsweeper re-review PR #98211 has been substantively updated to address both r1 P1 findings:
The body update path on this PR is silently disabled (per the Source change in this revision
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 Why this addresses both r1 P1 findingsr1 P1 #1 (preserve undefined TTS log semantics)The source change above is the literal fix. The new r1 P1 #2 (typed optional variable in test)
r1 P1 #3 (real behavior proof from a real setup)The new 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 Vitest output (commit
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@sliverp @Patrick-Erichsen — friendly ping for awareness on this PR. PR #98211 (commit
Where I am: the proof is the best I can do without standing up a real QQBot bot account (the surface is the
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:
I'm pinging you because:
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: |
|
Closing this PR — the issue surface (UTF-16 surrogate safety in log previews) was addressed in the maintainer canonical pass 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 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. |
What Problem This Solves
extensions/qqbot/src/engine/gateway/gateway.ts:64builds theonMessageSentinfo log line withmeta.ttsText?.slice(0, 30). JavaScript.sliceoperates 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\uD83Chalf 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 canonicaltruncateUtf16Safehelper fromopenclaw/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
truncateUtf16Safeinopenclaw/plugin-sdk/text-utility-runtimefor 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'sonMessageSentcallback 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: whenmeta.ttsTextisundefined(the normal state for non-TTS sends), the log line still readsttsText=undefined, notttsText=. 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 throughtruncateUtf16Safewhen text is present.The
?optional-chain in the original code is folded into themeta.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:truncateUtf16Safefromopenclaw/plugin-sdk/text-utility-runtime(1 line, alphabetical position).meta.ttsText?.slice(0, 30)with the absent-aware ternary at line 65 in theonMessageSentinfo log line. Net source change: +2/-2.extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts(new): 5 unit tests covering thetruncateUtf16Safehelper at the exact 30-char boundary that theonMessageSentsite 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 pair0xD83C 0xDF89) placed immediately afterN-1ASCIIacharacters so it straddles the 30-char truncation point. This is the exact boundary that the productiononMessageSentlog site exercises:🎉= U+1F389, high surrogate0xD83C, low surrogate0xDF89. The loned83chalf 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=undefinedfor absent TTS, matching the previousmeta.ttsText?.slice(0, 30)log line:The previous
?.slicereturnedundefinedfor 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):
Negative control — temporarily reverting line 65 from the absent-aware ternary back to
meta.ttsText?.slice(0, 30)(the main behavior):undefined" test still passes (the absent case in the reverted?.sliceform also rendersttsText=undefined).?.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
?.sliceform.Verification:
node scripts/run-vitest.mjs run --reporter=verbose extensions/qqbot/src/engine/gateway/gateway.utf16-truncation.test.ts→ 5/5 passed in 891msnode 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=0pnpm tsgo:extensions→ silent (no type errors)Diff scope
gateway.ts(1 import + 1 ternary replacing the?.sliceform, net +1).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).openclaw/main @ 6cb82eaab8.Security & Privacy
truncateUtf16Safehelper is canonical SDK surface, not a monkey-patch.Compatibility
ttsText=undefinedpreserved).onMessageSentlog line in 1 source file. No public API.What was not tested
.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
meta.ttsTextstill renders as the string"undefined"; emoji at boundary now drops whole (surrogate-safe) instead of emitting a lone high surrogate.Related
truncateUtf16Safe)fix(qqbot): drop dangling surrogates in streaming-c2c onDeliver previewopenclaw/plugin-sdk/text-utility-runtimeexportstruncateUtf16Safe(input, maxLen)andsliceUtf16Safe(input, start, end).onMessageSentcallback is the public event hook thatsender.ts:228invokes when a message is sent; the log line is captured by the gateway's info logger.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 actualtruncateUtf16Safehelper fromopenclaw/plugin-sdk/text-utility-runtimeagainst the same boundary input the productiononMessageSentlog site exercises, with hex-tail output to make the surrogate-difference explicit. Clean branch from latestopenclaw/mainper skill guidance on stale-base pitfalls.