fix: align HTTP projection with binary protocol (nested unknowns + ID format)#19
Merged
jmccarthy merged 2 commits intostrongdm:mainfrom Feb 22, 2026
Conversation
da59b8a to
3264886
Compare
The HTTP projection engine's render_type_ref function silently dropped all msgpack tags not listed in the registry descriptor for nested types (AssistantTurn, ToolCallItem, etc.). Extension fields written by newer clients survived perfectly through the binary protocol but were stripped by the HTTP API. Root cause: project_msgpack (top-level) collected unknown tags into an unknown map when include_unknown=true, but render_type_ref (nested objects) only iterated over descriptor fields and discarded everything else, ignoring include_unknown entirely. Fix: render_type_ref now collects unknown tags into an _unknown key on the nested JSON object when include_unknown is true. The key is omitted when there are no unknown tags or when the option is disabled. Three new tests verify nested ref objects, array ref items, and the disabled case. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
3264886 to
d05d3d4
Compare
Every HTTP endpoint serialized u64 IDs (context_id, turn_id, parent_turn_id, head_turn_id, session_id, etc.) as JSON strings via explicit .to_string() calls. The binary protocol returns native u64 integers, creating a type mismatch that required every HTTP client to implement custom string-to-integer deserialization (the SseUint64 pattern in both Rust and Go clients). This commit: - Adds a format_id(id, format) helper that returns either a JSON string or JSON number depending on the requested U64Format - Applies it to ALL endpoints: create, fork, append, search, children, provenance, turns, filesystem snapshots, sessions, and the context_to_json helper - The GET turns endpoint already accepts ?u64_format=string|number and now applies it consistently to both envelope and payload fields - All other endpoints default to U64Format::Number, matching the binary protocol Both Rust and Go clients already handle numeric IDs via their SseUint64 deserializers, so this is backward-compatible at the client level. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
d05d3d4 to
43af4d5
Compare
jmccarthy
approved these changes
Feb 22, 2026
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.
Overview
This PR fixes two related mismatches between the cxdb HTTP API and the binary protocol, both causing data loss or type inconsistency when clients read data over HTTP that was written via the binary protocol.
Fix 1: Propagate
include_unknowninto nested type projectionsProblem
The HTTP projection engine silently drops msgpack tags that are not listed in the registry descriptor for nested types (AssistantTurn, ToolCallItem, etc.). Extension fields written by newer clients — such as Amplifier adding
event_blobs(tag 11 on AssistantTurn) orchild_context_id(tag 12 on ToolCallItem) — survive perfectly through the binary protocol but are stripped by the HTTP API.Concretely:
include_unknown=1include_unknown=1: Theunknown: {}dict is empty for nested objects — the server does not cascadeinclude_unknowninto nested type projectionsRoot Cause
Asymmetric handling of unknown tags between top-level and nested projection:
project_msgpack(top-levelConversationItem): correctly collected unknown tags into anunknownmap wheninclude_unknown=truerender_type_ref(nested objects likeAssistantTurn,ToolCallItem): only iterated over fields declared in the descriptor and discarded everything else, completely ignoring theinclude_unknownflagFix
render_type_refinserver/src/projection/mod.rsnow:include_unknownis true and there are unknown entries, adds them to an_unknownkey on the nested JSON object_unknown(with leading underscore) to distinguish from the top-levelunknownkey, avoiding naming collisions_unknownentirely when there are no unknown tags or when the option is disabled — full backward compatibilityTests
Three new tests in
server/tests/registry_projection.rs:test_include_unknown_nested_ref_unknowntest_include_unknown_nested_array_ref_unknowntest_include_unknown_false_nested_unknownis absent wheninclude_unknownis falseFix 2: Return u64 IDs as JSON numbers across all HTTP endpoints
Problem
Every HTTP endpoint serialized u64 IDs (
context_id,turn_id,parent_turn_id,head_turn_id,session_id, etc.) as JSON strings via explicit.to_string()calls. The binary protocol returns native u64 integers, creating a type mismatch that required every HTTP client to implement custom string-to-integer deserialization (theSseUint64pattern in both Rust and Go clients).Fix
format_id(id, format)helper that returns either a JSON string or a JSON number depending on the requestedU64Formatcontext_to_jsonhelperGET /v1/contexts/{id}/turnsendpoint already accepts?u64_format=string|numberand now applies it consistently to both envelope and payload fieldsU64Format::Number, matching the binary protocolBackward Compatibility
Both Rust and Go clients already handle numeric IDs via their
SseUint64deserializers (which accept both strings and numbers), so this is backward-compatible at the client level. Clients that need the old string behaviour on the turns endpoint can pass?u64_format=string.Note on
?u64_format=stringbackward-compatibility escape hatchThe turns endpoint retains a
?u64_format=stringquery parameter that lets callers opt back into the old string-serialized IDs. This was kept as a safety net during the transition, but it may be unnecessary — both the Rust and Go clients already deserialize numeric IDs natively, and theSseUint64adapters handle either format.If the team prefers, the
format_idhelper and theu64_formatparameter can be removed entirely in favour of always returningJsonValue::Number(id.into()). That would simplify the code further — happy to do that here if preferred.Test plan
cargo test)_unknownpropagation for ref and array-ref fields_unknownis absent wheninclude_unknown=false_unknownkey appears when there are no unknown tagsSseUint64deserializers in both clients accept numbers natively🤖 Generated with Amplifier