fix: harden proxy failure paths, UTF-8 transcript decoding, unknown-block assembly - #9
Merged
Conversation
…lock assembly Hardening pass over the observation core surfaced by adversarial review: - proxy: never append fabricated JSON after a response has started; sever the connection instead (mid-stream failure now matches what Claude would see talking to Anthropic directly) - proxy: handle upstream response-stream death (error/aborted), which previously had no listener — Claude hung until the 120s timeout and an unhandled 'error' could crash the process - proxy: only warn about non-SSE 200s when the request asked to stream (count_tokens and stream:false are healthy JSON responses) - proxy: respect client backpressure on the observed SSE path - transcript-observer: decode through a persistent StringDecoder so a multibyte UTF-8 character split across polls is not corrupted - message-assembler: pass unknown content block types (server_tool_use, redacted_thinking, future kinds) through raw instead of dropping them, so deltas can no longer corrupt the previous block and output matches claude -p; unknown blocks never become lastToolUse
…t parse - proxy SSE observer now decodes through a persistent StringDecoder, so a multibyte char split across network chunks isn't corrupted in clarp's observed stream (raw bytes to Claude were already untouched). - message-assembler only JSON-parses an unknown block's string input when input_json_delta actually accumulated it, so a raw block carrying a literal string input passes through verbatim. - Tighten the mid-stream severance test to assert abrupt termination (not a clean end), and add a UTF-8-split observation test.
dn00
force-pushed
the
fix/observation-hardening
branch
from
June 10, 2026 10:11
af3cb74 to
53965aa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardening pass over the observation core (proxy, transcript observer, message assembler). None of these change behavior for well-formed traffic — the proxy fixes only change failure-path behavior, the assembler fix only changes unknown-shape behavior.
Proxy
proxy_errorJSON blob to the client response — mid-stream, that meant Claude received a 200text/event-streamending in JSON Anthropic never sent. Now: 502 JSON only before headers; abrupt severance after.error/abortedlistener — Claude hung until the 120s timeout, and an unhandled streamerrorcould crash the process. Now the client connection is severed immediately, matching what Claude would see talking to Anthropic directly.count_tokensandstream: falseresponses are healthy JSON.pause/drain/resume).Transcript observer
StringDecoderso a multibyte UTF-8 character (emoji/CJK) split across two polls is no longer corrupted into replacement characters.Message assembler
content_block_starttypes (server_tool_use,redacted_thinking, future kinds) previously pushed nothing, so their deltas landed on the previous block and corrupted it. Now unknown blocks pass through raw — exactly asclaude -pemits them — withinput_json_deltaaccumulation, and they never becomelastToolUse.Testing
tsc --noEmitclean); existingmessage-assembler.test.tspasses unchanged.upstreamBaseUrloverride and a fake upstream (mid-stream severance asserts exact client bytes).npm run parity:streamvs nativeclaude -p: 20/20 results on both sides,assistant_count_delta: 0,result_count_delta: 0, no suspicious assistant texts.🤖 Generated with Claude Code