Skip to content

[Bug]: Recognize reordered and escaped freeform wrapper keys in direct SSE previews #5151

Closed

Description

@lidge-jun

Client or integration

Direct HTTP/API client

Area

Streaming

Summary

Direct Responses SSE can publish raw JSON for a freeform wrapper with a reordered or escaped input property, then complete with its unwrapped value. For example, {"metadata":1,"input":"cmd"} and {"\u0069nput":"cmd"} can preview JSON and complete as cmd.

This is present before #5129, which extracts the existing decoder and repairs routed preview behavior. The routed restoration path suppresses these unchanged object prefixes; this issue covers the remaining direct bridge path. Related: #5047, #5113, and the review discussion.

Reproduction

Static source trace at 4e7d7132d8f8a7d46e460b48c5104a6d5ced4567; no local runtime reproduction was executed.

  1. Declare a custom freeform exec tool on the direct Responses bridge.
  2. Feed a fake upstream function call whose complete arguments are either example above, across every split boundary and characterwise chunks.
  3. Capture the custom-tool input deltas and final item input.
  4. The literal-first-property matcher returns unrecognized object text unchanged, while completion parses the object and extracts input. Assert that preview never publishes wrapper syntax that completion removes.

Version

4e7d713; still present at PR #5129 head b1b604e.

Operating system

Source review on macOS; the decoder is platform-independent. Execution proof must come from hosted CI.

Provider and model

Not provider-specific. Use a fake upstream without credentials.

Logs or error output

No live traffic or runtime logs collected. This report is based on the source trace and existing contract tests.

Screenshots and supporting files

Redacted configuration

A custom freeform exec declaration, a fake upstream event fixture and an isolated translator budget. No live provider, user state directory or credentials are required.

Implementation path

  1. Replace literal-only detection at the shared progressive decoder with bounded JSON object scanning. Track strings, escapes, nested values and top-level property boundaries. Decode property names according to JSON rules and recognize canonical input after preceding properties.
  2. Hold uncertainty until classification is possible. Preserve completion's own-input precedence and exactly-one-string-fallback-candidate rule. If no wrapper applies, release the original bytes as deltas rather than permanently suppressing ambiguous raw objects.
  3. Preserve fence/patch holds, tool-name ownership, caller-owned accounting and terminal/disposal release. Update the transport owner documentation with the precise buffering policy.
  4. Add sibling focused regressions and register new files in both test-layout inventories. Do not grow a capped bridge test file or raise the ratchet.

Acceptance criteria and verification

  • Reordered and escaped canonical keys are covered across every split and characterwise chunks; preview and authoritative completion agree without exposing wrapper syntax.
  • {"code":"a","script":"b"} and {"code":1} retain original raw delta and final bytes once classified as non-wrappers.
  • Canonical ordinary input and plain raw bodies remain progressive where their classification is safe.
  • Cover nested preceding values, escaped fallback keys, non-string input, tool-name negatives, cancellation and buffer release.
  • Retain explicit duplicate-key and late-invalid-input limitations rather than claiming a universal invariant unsupported by the buffering policy.
  • Hosted regression execution and exact-head applicable CI are required; this issue claims no passing runtime result.

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added 4 commits that reference this issue on Sep 19, 2026
    fff5e87
    503bd3e
    7aa3069
    4853f90
  2. lidge-jun commented on Sep 19, 2026

    @lidge-jun
    OwnerAuthor

    Closed by #5203, squash-merged into dev as 4853f90d9a.

    The direct SSE preview compared the wrapper prefix as a literal, so a freeform payload whose keys arrived reordered — or whose key was escaped — was invisible to it. That comparison is now a JSON scan (src/responses/freeform-wrapper-scan.ts), and the reordered and escaped cases are covered at every split point along with the case where the budget is exceeded and nothing is published.

    One consequence is worth stating plainly rather than leaving for someone to discover. A parseable JSON object that is not a wrapper now leaves the direct bridge in one piece when the object closes, instead of streaming through. This is unavoidable: input may follow any property, so bytes emitted before the object closes can be contradicted by the completion that follows. The routed path has held the same body since #5047, so the two paths converge rather than diverge. Non-object bodies — exec programs, patch envelopes — are unaffected. The cost is recorded in structure/transports/responses.md, and the bridge test comments that asserted the old timing were corrected rather than left to mislead.

    Adversarial review changed the release shape here too, and the reviewer's prescription was better than the first attempt: the over-budget buffer no longer re-parses on every delta, and a release parse is allowed only when the scanner has actually seen the object close. A hold short on buffer and a hold short on budget both stay held. The reasoning is that a delta an over-budget buffer would have emitted arrives at the same moment as the authoritative completion behind it, so it buys the client nothing. There is a regression pinning that with a value made of 4097 closing braces.

    Three transport hardening carries landed in the same PR with their authors credited, and none of them went in as submitted. The varint length bound in the Grok coupon reader did not actually exist, because continuation bytes carrying no payload contribute zero and never trip the safe-integer guard — twenty 0x80 bytes followed by 0x00 decoded as a well-formed zero, and an overlong zero length normalizes a malformed response into an empty coupon list. The explicit 10-byte protobuf bound is now there with a regression. The Devin reasoning-signature carry, as submitted, preserved pairing by discarding every reasoning block but the last, which loses real text when several blocks are signed; text is now replayed in full and the signature is attached only when the text being sent is the text that signature vouches for. The hosted-tool index carry was correct but its regression could not tell "first stripped container" from "index 0", since every container in the fixture was stripped.

    Evidence is exact-head hosted CI at 7aa30697f3159b69a7c6579d43ad5708b7f2175c: test 1/4 through 4/4 and gates successful. test 3/4 matters specifically — before the file-size ratchet on dev was repaired it aborted at batch 3 of 29 and the remaining 26 never ran, which left tests/providers/devin-hardening.test.ts unexecuted; it now runs to completion. No local suite was run in this lane.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions