Skip to content

Commit 22d3312

Browse files
authored
Merge pull request #19 from colombod/fix/http-projection-include-unknown-nested
fix: align HTTP projection with binary protocol (nested unknowns + ID format)
2 parents 569ac5a + 43af4d5 commit 22d3312

3 files changed

Lines changed: 290 additions & 30 deletions

File tree

server/src/http/mod.rs

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ fn handle_request(
250250
.iter()
251251
.map(|s| {
252252
let mut session_obj = json!({
253-
"session_id": s.session_id.to_string(),
253+
"session_id": format_id(s.session_id, &U64Format::Number),
254254
"client_tag": s.client_tag,
255255
"connected_at": s.connected_at,
256256
"last_activity_at": s.last_activity_at,
@@ -302,8 +302,8 @@ fn handle_request(
302302
});
303303

304304
let resp = json!({
305-
"context_id": head.context_id.to_string(),
306-
"head_turn_id": head.head_turn_id.to_string(),
305+
"context_id": format_id(head.context_id, &U64Format::Number),
306+
"head_turn_id": format_id(head.head_turn_id, &U64Format::Number),
307307
"head_depth": head.head_depth,
308308
});
309309
let bytes = serde_json::to_vec(&resp)
@@ -335,8 +335,8 @@ fn handle_request(
335335
});
336336

337337
let resp = json!({
338-
"context_id": head.context_id.to_string(),
339-
"head_turn_id": head.head_turn_id.to_string(),
338+
"context_id": format_id(head.context_id, &U64Format::Number),
339+
"head_turn_id": format_id(head.head_turn_id, &U64Format::Number),
340340
"head_depth": head.head_depth,
341341
});
342342
let bytes = serde_json::to_vec(&resp)
@@ -368,8 +368,8 @@ fn handle_request(
368368
});
369369

370370
let resp = json!({
371-
"context_id": head.context_id.to_string(),
372-
"head_turn_id": head.head_turn_id.to_string(),
371+
"context_id": format_id(head.context_id, &U64Format::Number),
372+
"head_turn_id": format_id(head.head_turn_id, &U64Format::Number),
373373
"head_depth": head.head_depth,
374374
});
375375
let bytes = serde_json::to_vec(&resp)
@@ -423,8 +423,8 @@ fn handle_request(
423423
let is_live = session.is_some();
424424

425425
let mut obj = json!({
426-
"context_id": context_id.to_string(),
427-
"head_turn_id": head.head_turn_id.to_string(),
426+
"context_id": format_id(context_id, &U64Format::Number),
427+
"head_turn_id": format_id(head.head_turn_id, &U64Format::Number),
428428
"head_depth": head.head_depth,
429429
"created_at_unix_ms": head.created_at_unix_ms,
430430
"is_live": is_live,
@@ -582,7 +582,7 @@ fn handle_request(
582582
.collect();
583583

584584
let resp = json!({
585-
"context_id": context_id.to_string(),
585+
"context_id": format_id(context_id, &U64Format::Number),
586586
"recursive": recursive,
587587
"count": children.len(),
588588
"children": children,
@@ -622,18 +622,18 @@ fn handle_request(
622622
prov_with_server_info.client_address = session_peer_addr;
623623
}
624624
json!({
625-
"context_id": context_id.to_string(),
625+
"context_id": format_id(context_id, &U64Format::Number),
626626
"provenance": prov_with_server_info,
627627
})
628628
} else {
629629
json!({
630-
"context_id": context_id.to_string(),
630+
"context_id": format_id(context_id, &U64Format::Number),
631631
"provenance": null,
632632
})
633633
}
634634
} else {
635635
json!({
636-
"context_id": context_id.to_string(),
636+
"context_id": format_id(context_id, &U64Format::Number),
637637
"provenance": null,
638638
})
639639
};
@@ -719,8 +719,8 @@ fn handle_request(
719719
}
720720

721721
let resp = json!({
722-
"context_id": context_id.to_string(),
723-
"turn_id": record.turn_id.to_string(),
722+
"context_id": format_id(context_id, &U64Format::Number),
723+
"turn_id": format_id(record.turn_id, &U64Format::Number),
724724
"depth": record.depth,
725725
"content_hash": hex::encode(hash.as_bytes()),
726726
});
@@ -829,11 +829,11 @@ fn handle_request(
829829
let mut turn_obj = Map::new();
830830
turn_obj.insert(
831831
"turn_id".into(),
832-
JsonValue::String(item.record.turn_id.to_string()),
832+
format_id(item.record.turn_id, &u64_format),
833833
);
834834
turn_obj.insert(
835835
"parent_turn_id".into(),
836-
JsonValue::String(item.record.parent_turn_id.to_string()),
836+
format_id(item.record.parent_turn_id, &u64_format),
837837
);
838838
turn_obj.insert("depth".into(), JsonValue::Number(item.record.depth.into()));
839839
turn_obj.insert(
@@ -913,10 +913,12 @@ fn handle_request(
913913
out_turns.push(JsonValue::Object(turn_obj));
914914
}
915915

916-
let next_before = turns.first().map(|t| t.record.turn_id.to_string());
916+
let next_before = turns
917+
.first()
918+
.map(|t| format_id(t.record.turn_id, &u64_format));
917919
let meta = json!({
918-
"context_id": context_id.to_string(),
919-
"head_turn_id": head.head_turn_id.to_string(),
920+
"context_id": format_id(context_id, &u64_format),
921+
"head_turn_id": format_id(head.head_turn_id, &u64_format),
920922
"head_depth": head.head_depth,
921923
"registry_bundle_id": registry.last_bundle_id(),
922924
});
@@ -1012,7 +1014,7 @@ fn handle_request(
10121014
.collect();
10131015

10141016
let resp = json!({
1015-
"turn_id": turn_id.to_string(),
1017+
"turn_id": format_id(turn_id, &U64Format::Number),
10161018
"path": path,
10171019
"fs_root_hash": hex::encode(fs_root),
10181020
"entries": entries_json,
@@ -1057,7 +1059,7 @@ fn handle_request(
10571059
EntryKind::Symlink => "symlink",
10581060
};
10591061
let resp = json!({
1060-
"turn_id": turn_id.to_string(),
1062+
"turn_id": format_id(turn_id, &U64Format::Number),
10611063
"path": path,
10621064
"name": entry.name,
10631065
"kind": kind_str,
@@ -1142,7 +1144,7 @@ fn handle_request(
11421144
.collect();
11431145

11441146
let resp = json!({
1145-
"turn_id": turn_id.to_string(),
1147+
"turn_id": format_id(turn_id, &U64Format::Number),
11461148
"path": path,
11471149
"fs_root_hash": hex::encode(fs_root),
11481150
"entries": entries_json,
@@ -1324,8 +1326,8 @@ fn context_to_json(
13241326
.filter(|t| !t.is_empty());
13251327

13261328
let mut obj = json!({
1327-
"context_id": head.context_id.to_string(),
1328-
"head_turn_id": head.head_turn_id.to_string(),
1329+
"context_id": format_id(head.context_id, &U64Format::Number),
1330+
"head_turn_id": format_id(head.head_turn_id, &U64Format::Number),
13291331
"head_depth": head.head_depth,
13301332
"created_at_unix_ms": head.created_at_unix_ms,
13311333
"is_live": is_live,
@@ -1335,7 +1337,7 @@ fn context_to_json(
13351337
obj["client_tag"] = JsonValue::String(tag);
13361338
}
13371339
if let Some(sid) = session_id {
1338-
obj["session_id"] = JsonValue::String(sid.to_string());
1340+
obj["session_id"] = format_id(sid, &U64Format::Number);
13391341
}
13401342
if let Some(ts) = last_activity_at {
13411343
obj["last_activity_at"] = JsonValue::Number(ts.into());
@@ -1379,12 +1381,12 @@ fn context_to_json(
13791381
let child_context_ids = store.child_context_ids(context_id);
13801382
let child_context_ids_json: Vec<JsonValue> = child_context_ids
13811383
.iter()
1382-
.map(|id| JsonValue::String(id.to_string()))
1384+
.map(|id| format_id(*id, &U64Format::Number))
13831385
.collect();
13841386

13851387
obj["lineage"] = json!({
1386-
"parent_context_id": parent_context_id.map(|v| v.to_string()),
1387-
"root_context_id": root_context_id.map(|v| v.to_string()),
1388+
"parent_context_id": parent_context_id.map(|v| format_id(v, &U64Format::Number)),
1389+
"root_context_id": root_context_id.map(|v| format_id(v, &U64Format::Number)),
13881390
"spawn_reason": spawn_reason,
13891391
"child_context_count": child_context_ids.len(),
13901392
"child_context_ids": child_context_ids_json,
@@ -1465,6 +1467,21 @@ fn get_optional_u64(body: &JsonValue, key: &str) -> Result<Option<u64>> {
14651467
}
14661468
}
14671469

1470+
/// Format a `u64` ID according to the requested `U64Format`.
1471+
///
1472+
/// When `U64Format::String`, the value is returned as a JSON string (e.g. `"42"`).
1473+
/// When `U64Format::Number`, the value is returned as a JSON number (e.g. `42`).
1474+
///
1475+
/// This is used for envelope/metadata fields (turn_id, context_id, etc.) so
1476+
/// that the HTTP API can match the binary protocol's native integer
1477+
/// representation when callers opt in via `?u64_format=number`.
1478+
fn format_id(id: u64, format: &U64Format) -> JsonValue {
1479+
match format {
1480+
U64Format::String => JsonValue::String(id.to_string()),
1481+
U64Format::Number => JsonValue::Number(id.into()),
1482+
}
1483+
}
1484+
14681485
fn extract_http_client_tag(request: &tiny_http::Request) -> String {
14691486
for name in ["X-CXDB-Client-Tag", "X-Client-Tag"] {
14701487
if let Some(header) = request.headers().iter().find(|h| h.field.equiv(name)) {

server/src/projection/mod.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,14 @@ fn render_field_value(
166166
}
167167
}
168168

169-
/// Recursively project a value using a referenced type's descriptor
169+
/// Recursively project a value using a referenced type's descriptor.
170+
///
171+
/// When `options.include_unknown` is true, any tags present in the msgpack
172+
/// payload but absent from the type descriptor are collected into an
173+
/// `"_unknown"` key on the returned object. This mirrors the top-level
174+
/// `project_msgpack` behaviour and ensures that clients reading via the HTTP
175+
/// API can discover extension fields added by newer writers (e.g. Amplifier
176+
/// adding `event_blobs` or `child_context_id` to a ToolCallItem).
170177
fn render_type_ref(
171178
value: &Value,
172179
type_ref: &str,
@@ -193,6 +200,21 @@ fn render_type_ref(
193200
}
194201
}
195202

203+
// Propagate include_unknown into nested types — collect tags that the
204+
// descriptor doesn't know about so they surface through the HTTP API.
205+
if options.include_unknown {
206+
let mut unknown = Map::new();
207+
for (tag, val) in map.iter() {
208+
if type_spec.fields.contains_key(tag) {
209+
continue;
210+
}
211+
unknown.insert(tag.to_string(), render_value(val, options));
212+
}
213+
if !unknown.is_empty() {
214+
data.insert("_unknown".into(), JsonValue::Object(unknown));
215+
}
216+
}
217+
196218
JsonValue::Object(data)
197219
}
198220

0 commit comments

Comments
 (0)