Skip to content

fix: harden proxy failure paths, UTF-8 transcript decoding, unknown-block assembly - #9

Merged
dn00 merged 3 commits into
mainfrom
fix/observation-hardening
Jun 10, 2026
Merged

fix: harden proxy failure paths, UTF-8 transcript decoding, unknown-block assembly#9
dn00 merged 3 commits into
mainfrom
fix/observation-hardening

Conversation

@dn00

@dn00 dn00 commented Jun 10, 2026

Copy link
Copy Markdown
Owner

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

  • Never fabricate bytes after a response has started. The upstream-error handler used to unconditionally append a proxy_error JSON blob to the client response — mid-stream, that meant Claude received a 200 text/event-stream ending in JSON Anthropic never sent. Now: 502 JSON only before headers; abrupt severance after.
  • Handle upstream response-stream death. A mid-stream connection drop (e.g. ECONNRESET on a network blip) surfaces on the response stream, which had no error/aborted listener — Claude hung until the 120s timeout, and an unhandled stream error could crash the process. Now the client connection is severed immediately, matching what Claude would see talking to Anthropic directly.
  • Only warn about non-SSE 200s when the request asked to streamcount_tokens and stream: false responses are healthy JSON.
  • Respect client backpressure on the observed SSE path (pause/drain/resume).

Transcript observer

  • Decode through a persistent StringDecoder so a multibyte UTF-8 character (emoji/CJK) split across two polls is no longer corrupted into replacement characters.

Message assembler

  • Unknown content_block_start types (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 as claude -p emits them — with input_json_delta accumulation, and they never become lastToolUse.

Testing

  • 393/393 unit tests pass (tsc --noEmit clean); existing message-assembler.test.ts passes unchanged.
  • 6 new regression tests verified to fail against the unfixed code.
  • Proxy failure paths covered via a new test-only upstreamBaseUrl override and a fake upstream (mid-stream severance asserts exact client bytes).
  • Live npm run parity:stream vs native claude -p: 20/20 results on both sides, assistant_count_delta: 0, result_count_delta: 0, no suspicious assistant texts.

🤖 Generated with Claude Code

dn00 added 3 commits June 9, 2026 22:41
…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
dn00 force-pushed the fix/observation-hardening branch from af3cb74 to 53965aa Compare June 10, 2026 10:11
@dn00
dn00 merged commit 89cb504 into main Jun 10, 2026
10 checks passed
@dn00
dn00 deleted the fix/observation-hardening branch June 10, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant