fix(message): guard ProviderData against empty RawMessage entries#43
Merged
Conversation
…ge footgun #42 normalized ToolCall.Arguments (a bare json.RawMessage field) but left Reasoning.ProviderData - a map[string]json.RawMessage carrying the exact same encoding/json footgun one layer of map indirection away - completely unguarded, which is why 'json: error calling MarshalJSON for type json.RawMessage: unexpected end of JSON input' recurred on a binary that already had #42's fix. Systematic guard, at the message package (mirroring ToolCall's own safeArguments/MarshalJSON pattern) rather than patching individual call sites: - ProviderData.Get(family) is the one choke point every transcoder must use to read an entry; it treats a present-but-zero-length entry as absent, so a transcoder never unmarshals or replays a poisoned value (anthropic's transcoder previously failed with a related but distinct 'bad anthropic reasoning data: unexpected end of JSON input' via json.Unmarshal on such an entry; openai's transcoder silently emitted a spurious null wire item instead of dropping it). - ProviderData.MarshalJSON drops zero-length entries from the encoded object, guarding every direct marshal of a Reasoning part (the session log, the server journal, a chat.message plugin hook payload) regardless of whether any transcoder's Get ever ran. - Message.Normalize, called from engine.Session.append (the single ingest point every message passes through), deletes zero-length entries from the Go value itself at ingest time, so an in-memory session history never diverges from what LoadSession would reload (a map with one zero-length entry is 'non-empty' to encoding/json's omitempty, which only checks length, so leaving the entry in place would make the marshaled shape reload-unstable even though it no longer crashes). Anthropic, OpenAI Responses, and OpenAI-compat transcoders all now read ProviderData through Get instead of indexing the map directly. Tests reconstruct the incident at every layer required: message-encoder unit marshal (message_test.go), full transcode-request marshal for both anthropic and openai (provider/*/transcode_test.go), and the full worker-turn path - a scripted provider producing the incident's exact part shape, run through a real Session.Prompt and PursueGoal turn, persisted to and reloaded from the session log (engine/store_test.go, engine/goal_test.go). All reproduce the exact production error before this fix and pass after it.
|
Claude finished @andybons's task in 4m 10s —— View job PR Review: guard ProviderData against empty RawMessage entries
Verdict: LGTM. This correctly root-causes the recurring What it doesThree coordinated guards on
Verification I did on the trickier claims
Notes (all minor / optional — nothing blocking)
Consistent with AGENTS.md: TDD (incident-shaped tests at message, transcoder, persist, and goal-loop levels), red-verification called out, no raw sleeps, golden round-trip comparisons. Good work. |
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
Forensics round 2 on the recurring
MarshalJSON … unexpected end of JSON inputincident. The actual offending field ismessage.ProviderData(map[string]json.RawMessage) on Reasoning parts — one indirection away from #42'sToolCall.Argumentsguard, marshaled unguarded on every persist/journal/hook encode and read by direct map indexing in all three transcoders. A present-but-zero-length provider-data entry reproduces the incident error exactly (red-verified by reverting the fix); goal-supervised sessions hit it repeatedly because they run with reasoning enabled and replay provider data same-family.Guard applied at the type level mirroring #42's pattern, with the full worker-turn path (transcode + persist) covered by the incident-shaped test, not just the encoder.
Why #42 missed it: its repro matched the error string, not the site — the lesson (forensic fixes must re-run the failing workload shape) goes to AGENTS.md in the next docs pass.
Box forensics session
ses_01kx3se8wqexatnesyxmgj5b0q, box-pushed.🤖 Generated with Claude Code