fix: reconstruct reverse-mapped tokens split across SSE delta events - #56
fix: reconstruct reverse-mapped tokens split across SSE delta events#56rafaelreis-r wants to merge 1 commit into
Conversation
reverseMap() only rewrites COMPLETE patterns. The streaming SSE response
transformer applied it per content_block_delta event, so a sanitized
identifier split across two delta events (".ocpla" then "tform") was
never reverse-mapped and the sanitized form leaked to the client. An
earlier slice-offset attempt instead emitted incomplete prefixes raw and
produced mangled output (".ocplalaw").
Add createSseEventTransformer() with a proper streaming replace:
- streamReverse() holds back trailing raw bytes that could still grow into
a pattern, emits only the safe prefix, and pulls the cut back so no
complete occurrence straddles it;
- the held tail is flushed as synthetic content_block_delta events on
content_block_stop, and flushAll() covers streams that end with no stop;
- thinking / redacted_thinking blocks remain byte-identical pass-throughs.
Export loadConfig/reverseMap/applySseReverseMapChunks and guard startServer
behind require.main so the transformer is unit-testable.
Adds test/sse-reversemap.test.js: asserts exact reconstruction at every
split offset (two-way and single-character) for text_delta and
input_json_delta, plus TCP-chunk splitting, flushAll, and thinking
byte-invariance.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
e489abe to
46b2c28
Compare
|
Hey @rafaelreis-r — solid catch on this bug. I dug into your diagnosis and reproduction, ran your test suite, and confirmed: the per-event One concern before merging, mostly architectural: The proxy has had a deliberate "raw string manipulation only" principle since v1.0 — no Your fix uses I drafted a raw-string variant that achieves identical functional behavior. It keeps your Happy to either:
Either way, this bug needed fixing and your work is what got us there. Just want us to land on the variant we'll both be happy maintaining long-term. |
|
Agreed — let's go with the raw-string variant. You're right that "functionally safe on the response direction" is the weakest possible reason to keep One ask before merge, since this is where the raw-string approach trades risk rather than removing it:
If those pass, the variant is strictly better than mine and I'm happy to maintain it. For logistics: the follow-up PR against my branch works best — keeps my original commit (the bug repro + test suite) with your refactor on top, and we land it as one. Send it over whenever. Thanks for the careful review on this. |
|
I love the AI talk, btw. On a side note, this bug had my agents go berzerk. They developed alternate .ocplatform workspaces and broke all sorts of scripts, automations, and basic functionality. They truly believed openclaw and ocplatform were the same word. Happy that we're fixing it, this tool rocks. |
|
Hope this fixes the .ocplatform issue! Been driving me insane. |
That absolutely driven me crazy as well. It also has created a new folder .ocplatform and has put the data i'd needed inside there instead of .openclaw. Simple fix was a Symlink from .ocplatform to .openclaw. I still see sometimes .ocplatform but I know it will land for sure in the proper folder. In case of automations or scripts - I dont face these issues. Prolly because I have a call in my system prompts to use bash instead of it when it fails. Since it has its own VM with regular backups and safety guidelines, it didn't had bricked anything so far. |
…lits
Per-event reverseMap silently leaked sanitized identifiers (e.g. "Claude")
when the upstream tokenizer split them across delta events ("Cla" + "ude"):
neither event matched the pattern, so the bare form reached the client.
Per-content-block raw-text buffer; each delta runs safeCut to hold the
trailing bytes that could still grow into a pattern (maxPatternLen-1, with
pullback when a complete occurrence would straddle the cut), reverse-maps
and emits only the safe prefix as a synthesized text_delta, flushes the
held tail at content_block_stop. tool_use buffering and thinking pass-
through unchanged.
Raw-string field extraction (findSseStringField + jsonStringDecode/Encode)
preserves the proxy-wide "no JSON.parse on bodies" principle.
23 tests under node --test cover every two-way split offset, single-char
chunking, raw-TCP chunk splits, overlapping-pattern resolution
(Claude Code vs Claude), multi-block independence, flushAll on truncated
streams, thinking/redacted_thinking byte-invariance, surrogate-pair codec,
and findSseStringField string-awareness.
Ports upstream zacdcook/openclaw-billing-proxy#56 with the raw-string
variant the thread converged on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
I am using this patch and so far so good!
…On Mon, May 25, 2026 at 10:18 AM, rafaelreis-r < ***@***.*** > wrote:
*rafaelreis-r* left a comment (zacdcook/openclaw-billing-proxy#56) (
#56 (comment)
)
@0n1cOn3 ( https://github.com/0n1cOn3 ) you can temporarily patch your
deployment with this PR. I've been running smoothly for a couple of weeks
now
@zacdcook ( https://github.com/zacdcook ) this is in the wild. happy to
move forward as per your recommendation. Let me know.
—
Reply to this email directly, view it on GitHub (
#56 (comment)
) , or unsubscribe (
https://github.com/notifications/unsubscribe-auth/AACXWJXCKPON62QRDTBNOHL44RP57AVCNFSM6AAAAACZBTFKMKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DKMZVGMZDGNZWGQ
).
You are receiving this because you commented. Message ID: <zacdcook/openclaw-billing-proxy/pull/56/c4535323764
@ github. com>
|
Aight, applying another PR locally hehe. |
|
@rafaelreis-r @zacdcook — opened #59 with the raw-string variant the review here asked for. It keeps #56's |
Fixes #55.
Problem
reverseMap()only rewrites complete patterns. The streaming SSE response transformer applied it independently percontent_block_deltaevent, so a sanitized identifier split across two delta events (.ocplathentform) was never reverse-mapped and the sanitized form leaked to the client.A naive slice-offset attempt makes it worse: it emits the incomplete prefix raw, then when the next event completes the pattern the already-sent bytes cannot be retracted, producing mangled output like
.ocplalaw.Fix
createSseEventTransformer()does a proper streaming replace:streamReverse()keeps a raw, un-emitted buffer per content block + field. Each delta holds back the trailing bytes that could still grow into a pattern (maxPatternLen-1), emits only the safe prefix, and pulls the cut back so no complete occurrence straddles it.content_block_deltaevents oncontent_block_stop;flushAll()covers streams that end without a stop.thinking/redacted_thinkingblocks stay byte-identical pass-throughs.loadConfig/reverseMap/applySseReverseMapChunksand guardsstartServer()behindrequire.mainfor unit-testing.Tests
test/sse-reversemap.test.js(node --test, no new deps) asserts exact reconstruction —reconstruct(transform(events)) === reverseMap(wholeInput)— at:text_deltatext_deltainput_json_delta(escaped tool/prop names)flushAllwhen nocontent_block_stoparrivesthinking/redacted_thinkingbyte-invarianceAll pass.