Problem class: go-json-rawmessage-field-relayed-verbatim-not-stringified
I diagnosed the issue against the actual <project>-dev/<project> repo (fix commit 4da4775), ran the regression test green, and executed the RED mutation to prove non-vacuity, restoring internal/mesh/peer.go to its original sha256 fbdf6fe6…. Solution written to /workspace/solution.md:
body is relayed verbatim, not stringifiedProblem class: go-json-rawmessage-field-relayed-verbatim-not-stringified
Repo: <project>-dev/<project> · Fix commit: 4da4775 (DOGFOOD-MESH-2 / DOGFOOD-MESH-3)
Verdict: PASS c3dd795a
docs/mesh-protocol.md told consumers the RESPONSE body is a JSON-encoded string ("encode your body as a JSON string"), while the example above the table sent an object ("body":{"pong":true}). A consumer reading the doc literally could not predict the JSON type of a reply. The false rule came from a Python client's own json.dumps, mistaken for server behaviour.
The truth is in the forwarding path, not in any encode helper.
Server-side type — internal/mesh/message.go:
type Response struct {
Envelope
RequestID string `json:"request_id"`
Source PeerRef `json:"source"`
StatusCode int `json:"status_code"`
Body json.RawMessage `json:"body,omitempty"`
TraceID string `json:"trace_id"`
}
Body is json.RawMessage: the decoded frame keeps the original body bytes.
Forwarding path — internal/mesh/peer.go:
func (m *Mesh) forwardResponse(requestID string, data []byte) {
m.routesMu.Lock()
requesterID, ok := m.routes[requestID]
if ok { delete(m.routes, requestID) }
m.routesMu.Unlock()
if !ok { return }
m.mu.RLock()
conn, ok := m.connections[requesterID]
m.mu.RUnlock()
if ok {
_ = conn.Send(data) // <-- the raw received frame, byte for byte
}
}
data is the complete received frame. The server looks the route up by request_id and hands the same bytes to the requester. Nothing unmarshals and re-marshals, so the JSON type is the responder's choice:
| Responder sent | Requester gets |
|---|---|
object { "pong":true } |
object |
string "{\"pong\": true}" |
string |
array [1,2,3] |
array |
Secondary trap (MCP bridge). internal/mcp/messaging.go decodes the body into map[string]any and discards the error:
var body map[string]any
if len(reply.body) > 0 {
_ = json.Unmarshal(reply.body, &body) // error dropped
}
MeshRequestOutput.Body is map[string]any, so a string body fails the decode, body stays nil, and the tool result serialises as "body": null — the string content is silently lost.
Related correlation bug (DOGFOOD-MESH-3). ERROR frames take the same forwardResponse path as RESPONSE frames, and request_id is the failed REQUEST's message_id. A client correlating on message_id never matched a server ERROR and hung to timeout.
Server behaviour is correct and unchanged; the fix is docs + a non-vacuous regression test.
docs/mesh-protocol.md-| `body` | string | JSON-encoded payload (the field is `json.RawMessage`; encode your body as a JSON string) |
+| `body` | any | Opaque JSON value (`json.RawMessage` server-side), relayed verbatim — the responder decides the JSON type: an object body arrives as an object, a string body as a string (see below) |
Add the "bodies are responder-controlled" explanation (keep the object example above the table) plus the MCP note:
Bodies are responder-controlled. A RESPONSE
bodyis an opaque JSON value, not a string: the server holds the frame's raw bytes (Response.Body json.RawMessage,internal/mesh/message.go:79) and hands the frame it received to the requester's connection unchanged (forwardResponsecallsconn.Send(data)with those bytes,internal/mesh/peer.go:402-420). Nothing unwraps the body, re-encodes it, or stringifies it, so the requester decodes the bytes the responder wrote and the JSON type is the responder's choice. …Consumer note: an MCP client can read the body only when it is an object — the
mesh_requesttool decodesreply.bodyinto amap[string]any(internal/mcp/messaging.go:266-271,internal/mcp/types.go:204-208) and discards the decode error, so a STRING body reaches that client as anullbodyfield rather than as a string.
Add the ERROR correlation rule in §ERROR and rewrite the correlation contract to cover both reply types:
request_idcarries the failed REQUEST'smessage_id— the same value a RESPONSE echoes … match onrequest_id(the reply's correlation field), never onmessage_id(the frame's own id).
examples/llm-mesh/README.md-- RESPONSE `body` is a JSON-encoded string on the wire (RawMessage quirk) —
- the bridge hides this from harnesses.
+- RESPONSE `body` is **responder-controlled and relayed verbatim**: the server keeps
+ the frame's raw bytes (`json.RawMessage`) and never re-encodes the value, so an
+ object body comes back as an object. This demo sees a JSON *string* only because
+ its own client stringifies the payload (`mesh/<project>_mesh.py`: `"body": json.dumps(body)`).
+ The `mesh_request` bridge decodes the body into a map, so a string body reaches an
+ MCP client as `"body": null`, not as a string (DOGFOOD-MESH-2).
The client comment in examples/llm-mesh/mesh/<project>_mesh.py becomes "body": json.dumps(body), # a STRING body — ours to choose.
skills/<project>-usage/SKILL.mdReplace the false "RESPONSE body goes on the wire as a JSON-encoded STRING" bullet with the opaque/verbatim rule plus the MCP "body": null consequence, and note the ERROR correlation rule.
internal/mesh/response_body_test.goFixtures are deliberately non-canonical so json.Marshal of the decoded value does not reproduce the sent bytes; any server-side re-encode fails the byte-identity assertion.
package mesh
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
"github.com/gorilla/websocket"
)
func TestResponseBodyRelayedVerbatim(t *testing.T) {
cases := []struct {
name string
body string
want any
}{
{
name: "object_body_stays_object",
body: `{ "pong" : true , "zeta" : 1 , "alpha" : 2 }`,
want: map[string]any{"pong": true, "zeta": float64(1), "alpha": float64(2)},
},
{
name: "string_body_stays_string",
body: `"{\"pong\": true, \"note\": \"a<b&c\"}"`,
want: `{"pong": true, "note": "a<b&c"}`,
},
{
name: "array_body_stays_array",
body: `[ 3, 1, 2 ]`,
want: []any{float64(3), float64(1), float64(2)},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if reencoded, err := json.Marshal(tc.want); err == nil && string(reencoded) == tc.body {
t.Fatalf("fixture not discriminating: json.Marshal of the decoded value reproduces the sent bytes %s", tc.body)
}
clientA, serverA := startAgentEndpoint(t)
clientB, serverB := startAgentEndpoint(t)
connA, connB := clientA(), clientB()
m := NewMesh(DefaultMeshConfig("relay"))
acceptAgent(t, m, "agent-a", connA, serverA())
acceptAgent(t, m, "agent-b", connB, serverB())
reqID := "req-verbatim-" + tc.name
req := &Request{
Envelope: Envelope{
Type: TypeRequest,
Version: 1,
MessageID: reqID,
Timestamp: time.Now(),
},
Source: PeerRef{AgentID: "agent-a"},
Target: PeerRef{AgentID: "agent-b"},
Method: "POST",
Path: "/echo",
Body: map[string]any{"echo": true},
TraceID: "trace-" + tc.name,
TimeoutMs: 5000,
}
data, err := Marshal(req)
if err != nil { t.Fatal(err) }
if err := connA.WriteMessage(websocket.TextMessage, data); err != nil { t.Fatal(err) }
_, reqEnv := readUntilType(t, connB, TypeRequest, 5*time.Second)
if reqEnv.MessageID != reqID {
t.Fatalf("forwarded REQUEST message_id = %q, want %q", reqEnv.MessageID, reqID)
}
sentBody := tc.body
frame := fmt.Sprintf(
`{"type":"RESPONSE","version":1,"message_id":"resp-%s","timestamp":"%s",`+
`"request_id":"%s","source":{"agent_id":"agent-b"},"status_code":200,`+
`"body":%s}`+"\n",
tc.name, time.Now().Format(time.RFC3339Nano), reqID, sentBody)
if !json.Valid([]byte(frame)) { t.Fatalf("test frame is not valid JSON: %s", frame) }
if err := connB.WriteMessage(websocket.TextMessage, []byte(frame)); err != nil { t.Fatal(err) }
msg, env := readUntilType(t, connA, TypeResponse, 5*time.Second)
if env.MessageID != "resp-"+tc.name {
t.Fatalf("relayed RESPONSE message_id = %q, want %q", env.MessageID, "resp-"+tc.name)
}
if string(msg) != frame {
t.Fatalf("frame was not relayed verbatim:\n sent: %s\n got: %s", frame, msg)
}
var resp Response
if err := json.Unmarshal(msg, &resp); err != nil { t.Fatal(err) }
if resp.RequestID != reqID {
t.Fatalf("response request_id = %q, want %q", resp.RequestID, reqID)
}
if got := string(resp.Body); got != sentBody {
t.Fatalf("body bytes were not relayed verbatim (re-encoded?):\n sent: %s\n got: %s", sentBody, got)
}
var decoded any
if err := json.Unmarshal(resp.Body, &decoded); err != nil {
t.Fatalf("decode relayed body %s: %v", resp.Body, err)
}
if !reflect.DeepEqual(decoded, tc.want) {
t.Fatalf("body type/value changed in relay: got %T %#v, want %T %#v", decoded, decoded, tc.want, tc.want)
}
if gotType, wantType := jsonTypeName(decoded), jsonTypeName(tc.want); gotType != wantType {
t.Fatalf("body JSON type changed in relay: got %s, want %s", gotType, wantType)
}
var asMap map[string]any
mapErr := json.Unmarshal(resp.Body, &asMap)
_, wantObject := tc.want.(map[string]any)
if wantObject && mapErr != nil {
t.Fatalf("object body must decode into the bridge's map[string]any: %v", mapErr)
}
if !wantObject && mapErr == nil {
t.Fatalf("expected the %s body to fail the bridge's map[string]any decode (so the bridge drops it), but it decoded to %v", tc.name, asMap)
}
if !wantObject {
out, err := json.Marshal(map[string]any{"body": asMap})
if err != nil { t.Fatal(err) }
if string(out) != `{"body":null}` {
t.Fatalf("bridge output for a %s body = %s, want {\"body\":null}", tc.name, out)
}
}
})
}
}
func jsonTypeName(v any) string {
switch v.(type) {
case map[string]any:
return "object"
case string:
return "string"
case []any:
return "array"
case float64:
return "number"
case bool:
return "boolean"
case nil:
return "null"
default:
return fmt.Sprintf("%T", v)
}
}
go test ./internal/mesh/ -run TestResponseBodyRelayedVerbatim -count=1 -race -v
--- PASS: TestResponseBodyRelayedVerbatim (0.01s)
--- PASS: TestResponseBodyRelayedVerbatim/object_body_stays_object (0.00s)
--- PASS: TestResponseBodyRelayedVerbatim/string_body_stays_string (0.00s)
--- PASS: TestResponseBodyRelayedVerbatim/array_body_stays_array (0.00s)
PASS
ok github.com/<project>-dev/<project>/internal/mesh 1.023s
Mutate forwardResponse to unmarshal + re-marshal the whole frame, then run the test. All three subtests must FAIL (keys sorted, whitespace compacted, </& escaped).
sha256sum internal/mesh/peer.go
# fbdf6fe6f3d8e106e27b0c513befb2b9d6aa56795f3b899e56b6ae23f4c8d3bf internal/mesh/peer.go
# mutate the forward call (forwardResponse, peer.go ~418):
# if ok {
# var v any
# if json.Unmarshal(data, &v) == nil {
# if b, err := json.Marshal(v); err == nil { data = b }
# }
# _ = conn.Send(data)
# }
gofmt -w internal/mesh/peer.go
go test ./internal/mesh/ -run TestResponseBodyRelayedVerbatim -count=1
Observed — all three fail on the re-encoded frame:
--- FAIL: .../object_body_stays_object
sent: ..."body":{ "pong" : true , "zeta" : 1 , "alpha" : 2 }}
got: {"body":{"alpha":2,"pong":true,"zeta":1}, ...}
--- FAIL: .../string_body_stays_string
got: {"body":"{\"pong\": true, \"note\": \"a\u003cb\u0026c\"}", ...}
--- FAIL: .../array_body_stays_array
got: {"body":[3,1,2], ...}
FAIL
Restore and confirm byte-identity:
git checkout -- internal/mesh/peer.go
sha256sum internal/mesh/peer.go
# fbdf6fe6f3d8e106e27b0c513befb2b9d6aa56795f3b899e56b6ae23f4c8d3bf internal/mesh/peer.go
go test ./internal/mesh/ -run TestResponseBodyRelayedVerbatim -count=1
# ok github.com/<project>-dev/<project>/internal/mesh
Against a freshly built binary on a scratch port: object reply → object received; string reply → string received; value and JSON type match the responder's; ERROR frame request_id equals the failed REQUEST's message_id.
make build 0 · go vet ./... 0 · go test ./... -count=1 0 (16/16 packages) · make test 0 · make docs-check 0 · make gofmt-check PASS (154 files, 0 rejected). No internal/mesh / internal/mcp behaviour changed.
| File | Change |
|---|---|
docs/mesh-protocol.md |
body: any + responder-controlled/verbatim section + MCP null consequence + ERROR request_id rule |
examples/llm-mesh/README.md |
Replace "RawMessage quirk" with verbatim note |
skills/<project>-usage/SKILL.md |
Replace false string rule; add MCP body:null and ERROR correlation |
examples/llm-mesh/mesh/<project>_mesh.py |
Comment: # a STRING body — ours to choose |
internal/mesh/response_body_test.go |
New TestResponseBodyRelayedVerbatim (3 subtests) |
When a relayed field is json.RawMessage and the forward path is conn.Send(receivedBytes), the JSON type on the wire is whatever the sender chose. Read the bytes handed to the connection write — not the encode helpers — prove it with two live peers, and pin it with fixtures a re-encode cannot reproduce.
# Evidence - Problem class: go-json-rawmessage-field-relayed-verbatim-not-stringified - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-18T17:30:42.548Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: a protocol doc told consumers to JSON-encode an opaque reply field as a STRING, while the same doc's example showed an OBJECT, and a consumer reading the doc literally could not predict what came back. Root cause class: when a relay server forwards a frame it must decide whether to re-encode the payload or pass the original bytes through. If the outbound field is typed json.RawMessage and the forward path calls conn.Send(data) with the received bytes, NOTHING stringifies or re-encodes: the JSON type on the wire is whatever the SENDER chose (object stays object, string stays string, array stays array), and a 'raw message quirk / body is a JSON-encoded string' doc line is a client-side artifact (a Python client that called json.dumps itself) elevated into a false server rule. Secondary trap: a downstream bridge that decodes the field into map[string]any and DISCARDS the decode error turns a string body into a null field, so a string-bodied reply silently loses its content at that hop. Diagnosis recipe: (1) read the forwarding path, not the encode helpers - the truth is the bytes handed to the connection write; (2) prove it live with two peers on a scratch port, replying once with an object and once with a string, and assert the received Python/JSON type matches what the responder sent (value_identical plus same type); (3) for a served doc, run the same probe against a freshly built binary at HEAD before editing prose, so the doc is aligned to measured behavior rather than to a recalled belief. Fix: state the wire truth in the doc's field table ('any | opaque JSON value, relayed verbatim - the responder decides the JSON type'), keep the object example, document the map-decode consequence for the bridge, and pin it with a regression test whose fixtures are deliberately non-canonical (unsorted keys, loose whitespace, < and &) so that json.Marshal of the decoded value does NOT reproduce the sent bytes - that makes a server-side re-encode fail the byte-identity assertion. Non-vacuity proof: mutate the forward call to json.Unmarshal+json.Marshal the frame and require the new subtests to FAIL, then restore and assert the source file's sha256 matches the pre-mutation value.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-json-rawmessage-field-relayed-verbatim-not-stringified", "provider": "openrouter", "solved_at": "2026-09-18T17:30:42.548Z", "version": ""}I diagnosed the issue against the actual <project>-dev/<project> repo (fix commit 4da4775), ran the regression test green, and executed the RED mutation to prove non-vacuity, restoring internal/mesh/peer.go to its original sha256 fbdf6fe6…. Solution written to /workspace/solution.md:
body is relayed verbatim, not stringifiedProblem class: go-json-rawmessage-field-relayed-verbatim-not-stringified
Repo: <project>-dev/<project> · Fix commit: 4da4775 (DOGFOOD-MESH-2 / DOGFOOD-MESH-3)
Verdict: PASS c3dd795a
docs/mesh-protocol.md told consumers the RESPONSE body is a JSON-encoded string ("encode your body as a JSON string"), while the example above the table sent an object ("body":{"pong":true}). A consumer reading the doc literally could not predict the JSON type of a reply. The false rule came from a Python client's own json.dumps, mistaken for server behaviour.
The truth is in the forwarding path, not in any encode helper.
Server-side type — internal/mesh/message.go:
type Response struct {
Envelope
RequestID string `json:"request_id"`
Source PeerRef `json:"source"`
StatusCode int `json:"status_code"`
Body json.RawMessage `json:"body,omitempty"`
TraceID string `json:"trace_id"`
}
Body is json.RawMessage: the decoded frame keeps the original body bytes.
Forwarding path — internal/mesh/peer.go:
func (m *Mesh) forwardResponse(requestID string, data []byte) {
m.routesMu.Lock()
requesterID, ok := m.routes[requestID]
if ok { delete(m.routes, requestID) }
m.routesMu.Unlock()
if !ok { return }
m.mu.RLock()
conn, ok := m.connections[requesterID]
m.mu.RUnlock()
if ok {
_ = conn.Send(data) // <-- the raw received frame, byte for byte
}
}
data is the complete received frame. The server looks the route up by request_id and hands the same bytes to the requester. Nothing unmarshals and re-marshals, so the JSON type is the responder's choice:
| Responder sent | Requester gets |
|---|---|
object { "pong":true } |
object |
string "{\"pong\": true}" |
string |
array [1,2,3] |
array |
Secondary trap (MCP bridge). internal/mcp/messaging.go decodes the body into map[string]any and discards the error:
var body map[string]any
if len(reply.body) > 0 {
_ = json.Unmarshal(reply.body, &body) // error dropped
}
MeshRequestOutput.Body is map[string]any, so a string body fails the decode, body stays nil, and the tool result serialises as "body": null — the string content is silently lost.
Related correlation bug (DOGFOOD-MESH-3). ERROR frames take the same forwardResponse path as RESPONSE frames, and request_id is the failed REQUEST's message_id. A client correlating on message_id never matched a server ERROR and hung to timeout.
Server behaviour is correct and unchanged; the fix is docs + a non-vacuous regression test.
docs/mesh-protocol.md-| `body` | string | JSON-encoded payload (the field is `json.RawMessage`; encode your body as a JSON string) |
+| `body` | any | Opaque JSON value (`json.RawMessage` server-side), relayed verbatim — the responder decides the JSON type: an object body arrives as an object, a string body as a string (see below) |
Add the "bodies are responder-controlled" explanation (keep the object example above the table) plus the MCP note:
Bodies are responder-controlled. A RESPONSE
bodyis an opaque JSON value, not a string: the server holds the frame's raw bytes (Response.Body json.RawMessage,internal/mesh/message.go:79) and hands the frame it received to the requester's connection unchanged (forwardResponsecallsconn.Send(data)with those bytes,internal/mesh/peer.go:402-420). Nothing unwraps the body, re-encodes it, or stringifies it, so the requester decodes the bytes the responder wrote and the JSON type is the responder's choice. …Consumer note: an MCP client can read the body only when it is an object — the
mesh_requesttool decodesreply.bodyinto amap[string]any(internal/mcp/messaging.go:266-271,internal/mcp/types.go:204-208) and discards the decode error, so a STRING body reaches that client as anullbodyfield rather than as a string.
Add the ERROR correlation rule in §ERROR and rewrite the correlation contract to cover both reply types:
request_idcarries the failed REQUEST'smessage_id— the same value a RESPONSE echoes … match onrequest_id(the reply's correlation field), never onmessage_id(the frame's own id).
examples/llm-mesh/README.md-- RESPONSE `body` is a JSON-encoded string on the wire (RawMessage quirk) —
- the bridge hides this from harnesses.
+- RESPONSE `body` is **responder-controlled and relayed verbatim**: the server keeps
+ the frame's raw bytes (`json.RawMessage`) and never re-encodes the value, so an
+ object body comes back as an object. This demo sees a JSON *string* only because
+ its own client stringifies the payload (`mesh/<project>_mesh.py`: `"body": json.dumps(body)`).
+ The `mesh_request` bridge decodes the body into a map, so a string body reaches an
+ MCP client as `"body": null`, not as a string (DOGFOOD-MESH-2).
The client comment in examples/llm-mesh/mesh/<project>_mesh.py becomes "body": json.dumps(body), # a STRING body — ours to choose.
skills/<project>-usage/SKILL.mdReplace the false "RESPONSE body goes on the wire as a JSON-encoded STRING" bullet with the opaque/verbatim rule plus the MCP "body": null consequence, and note the ERROR correlation rule.
internal/mesh/response_body_test.goFixtures are deliberately non-canonical so json.Marshal of the decoded value does not reproduce the sent bytes; any server-side re-encode fails the byte-identity assertion.
package mesh
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
"github.com/gorilla/websocket"
)
func TestResponseBodyRelayedVerbatim(t *testing.T) {
cases := []struct {
name string
body string
want any
}{
{
name: "object_body_stays_object",
body: `{ "pong" : true , "zeta" : 1 , "alpha" : 2 }`,
want: map[string]any{"pong": true, "zeta": float64(1), "alpha": float64(2)},
},
{
name: "string_body_stays_string",
body: `"{\"pong\": true, \"note\": \"a<b&c\"}"`,
want: `{"pong": true, "note": "a<b&c"}`,
},
{
name: "array_body_stays_array",
body: `[ 3, 1, 2 ]`,
want: []any{float64(3), float64(1), float64(2)},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if reencoded, err := json.Marshal(tc.want); err == nil && string(reencoded) == tc.body {
t.Fatalf("fixture not discriminating: json.Marshal of the decoded value reproduces the sent bytes %s", tc.body)
}
clientA, serverA := startAgentEndpoint(t)
clientB, serverB := startAgentEndpoint(t)
connA, connB := clientA(), clientB()
m := NewMesh(DefaultMeshConfig("relay"))
acceptAgent(t, m, "agent-a", connA, serverA())
acceptAgent(t, m, "agent-b", connB, serverB())
reqID := "req-verbatim-" + tc.name
req := &Request{
Envelope: Envelope{
Type: TypeRequest,
Version: 1,
MessageID: reqID,
Timestamp: time.Now(),
},
Source: PeerRef{AgentID: "agent-a"},
Target: PeerRef{AgentID: "agent-b"},
Method: "POST",
Path: "/echo",
Body: map[string]any{"echo": true},
TraceID: "trace-" + tc.name,
TimeoutMs: 5000,
}
data, err := Marshal(req)
if err != nil { t.Fatal(err) }
if err := connA.WriteMessage(websocket.TextMessage, data); err != nil { t.Fatal(err) }
_, reqEnv := readUntilType(t, connB, TypeRequest, 5*time.Second)
if reqEnv.MessageID != reqID {
t.Fatalf("forwarded REQUEST message_id = %q, want %q", reqEnv.MessageID, reqID)
}
sentBody := tc.body
frame := fmt.Sprintf(
`{"type":"RESPONSE","version":1,"message_id":"resp-%s","timestamp":"%s",`+
`"request_id":"%s","source":{"agent_id":"agent-b"},"status_code":200,`+
`"body":%s}`+"\n",
tc.name, time.Now().Format(time.RFC3339Nano), reqID, sentBody)
if !json.Valid([]byte(frame)) { t.Fatalf("test frame is not valid JSON: %s", frame) }
if err := connB.WriteMessage(websocket.TextMessage, []byte(frame)); err != nil { t.Fatal(err) }
msg, env := readUntilType(t, connA, TypeResponse, 5*time.Second)
if env.MessageID != "resp-"+tc.name {
t.Fatalf("relayed RESPONSE message_id = %q, want %q", env.MessageID, "resp-"+tc.name)
}
if string(msg) != frame {
t.Fatalf("frame was not relayed verbatim:\n sent: %s\n got: %s", frame, msg)
}
var resp Response
if err := json.Unmarshal(msg, &resp); err != nil { t.Fatal(err) }
if resp.RequestID != reqID {
t.Fatalf("response request_id = %q, want %q", resp.RequestID, reqID)
}
if got := string(resp.Body); got != sentBody {
t.Fatalf("body bytes were not relayed verbatim (re-encoded?):\n sent: %s\n got: %s", sentBody, got)
}
var decoded any
if err := json.Unmarshal(resp.Body, &decoded); err != nil {
t.Fatalf("decode relayed body %s: %v", resp.Body, err)
}
if !reflect.DeepEqual(decoded, tc.want) {
t.Fatalf("body type/value changed in relay: got %T %#v, want %T %#v", decoded, decoded, tc.want, tc.want)
}
if gotType, wantType := jsonTypeName(decoded), jsonTypeName(tc.want); gotType != wantType {
t.Fatalf("body JSON type changed in relay: got %s, want %s", gotType, wantType)
}
var asMap map[string]any
mapErr := json.Unmarshal(resp.Body, &asMap)
_, wantObject := tc.want.(map[string]any)
if wantObject && mapErr != nil {
t.Fatalf("object body must decode into the bridge's map[string]any: %v", mapErr)
}
if !wantObject && mapErr == nil {
t.Fatalf("expected the %s body to fail the bridge's map[string]any decode (so the bridge drops it), but it decoded to %v", tc.name, asMap)
}
if !wantObject {
out, err := json.Marshal(map[string]any{"body": asMap})
if err != nil { t.Fatal(err) }
if string(out) != `{"body":null}` {
t.Fatalf("bridge output for a %s body = %s, want {\"body\":null}", tc.name, out)
}
}
})
}
}
func jsonTypeName(v any) string {
switch v.(type) {
case map[string]any:
return "object"
case string:
return "string"
case []any:
return "array"
case float64:
return "number"
case bool:
return "boolean"
case nil:
return "null"
default:
return fmt.Sprintf("%T", v)
}
}
go test ./internal/mesh/ -run TestResponseBodyRelayedVerbatim -count=1 -race -v
--- PASS: TestResponseBodyRelayedVerbatim (0.01s)
--- PASS: TestResponseBodyRelayedVerbatim/object_body_stays_object (0.00s)
--- PASS: TestResponseBodyRelayedVerbatim/string_body_stays_string (0.00s)
--- PASS: TestResponseBodyRelayedVerbatim/array_body_stays_array (0.00s)
PASS
ok github.com/<project>-dev/<project>/internal/mesh 1.023s
Mutate forwardResponse to unmarshal + re-marshal the whole frame, then run the test. All three subtests must FAIL (keys sorted, whitespace compacted, </& escaped).
sha256sum internal/mesh/peer.go
# fbdf6fe6f3d8e106e27b0c513befb2b9d6aa56795f3b899e56b6ae23f4c8d3bf internal/mesh/peer.go
# mutate the forward call (forwardResponse, peer.go ~418):
# if ok {
# var v any
# if json.Unmarshal(data, &v) == nil {
# if b, err := json.Marshal(v); err == nil { data = b }
# }
# _ = conn.Send(data)
# }
gofmt -w internal/mesh/peer.go
go test ./internal/mesh/ -run TestResponseBodyRelayedVerbatim -count=1
Observed — all three fail on the re-encoded frame:
--- FAIL: .../object_body_stays_object
sent: ..."body":{ "pong" : true , "zeta" : 1 , "alpha" : 2 }}
got: {"body":{"alpha":2,"pong":true,"zeta":1}, ...}
--- FAIL: .../string_body_stays_string
got: {"body":"{\"pong\": true, \"note\": \"a\u003cb\u0026c\"}", ...}
--- FAIL: .../array_body_stays_array
got: {"body":[3,1,2], ...}
FAIL
Restore and confirm byte-identity:
git checkout -- internal/mesh/peer.go
sha256sum internal/mesh/peer.go
# fbdf6fe6f3d8e106e27b0c513befb2b9d6aa56795f3b899e56b6ae23f4c8d3bf internal/mesh/peer.go
go test ./internal/mesh/ -run TestResponseBodyRelayedVerbatim -count=1
# ok github.com/<project>-dev/<project>/internal/mesh
Against a freshly built binary on a scratch port: object reply → object received; string reply → string received; value and JSON type match the responder's; ERROR frame request_id equals the failed REQUEST's message_id.
make build 0 · go vet ./... 0 · go test ./... -count=1 0 (16/16 packages) · make test 0 · make docs-check 0 · make gofmt-check PASS (154 files, 0 rejected). No internal/mesh / internal/mcp behaviour changed.
| File | Change |
|---|---|
docs/mesh-protocol.md |
body: any + responder-controlled/verbatim section + MCP null consequence + ERROR request_id rule |
examples/llm-mesh/README.md |
Replace "RawMessage quirk" with verbatim note |
skills/<project>-usage/SKILL.md |
Replace false string rule; add MCP body:null and ERROR correlation |
examples/llm-mesh/mesh/<project>_mesh.py |
Comment: # a STRING body — ours to choose |
internal/mesh/response_body_test.go |
New TestResponseBodyRelayedVerbatim (3 subtests) |
When a relayed field is json.RawMessage and the forward path is conn.Send(receivedBytes), the JSON type on the wire is whatever the sender chose. Read the bytes handed to the connection write — not the encode helpers — prove it with two live peers, and pin it with fixtures a re-encode cannot reproduce.
# Evidence - Problem class: go-json-rawmessage-field-relayed-verbatim-not-stringified - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-18T17:30:42.548Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: a protocol doc told consumers to JSON-encode an opaque reply field as a STRING, while the same doc's example showed an OBJECT, and a consumer reading the doc literally could not predict what came back. Root cause class: when a relay server forwards a frame it must decide whether to re-encode the payload or pass the original bytes through. If the outbound field is typed json.RawMessage and the forward path calls conn.Send(data) with the received bytes, NOTHING stringifies or re-encodes: the JSON type on the wire is whatever the SENDER chose (object stays object, string stays string, array stays array), and a 'raw message quirk / body is a JSON-encoded string' doc line is a client-side artifact (a Python client that called json.dumps itself) elevated into a false server rule. Secondary trap: a downstream bridge that decodes the field into map[string]any and DISCARDS the decode error turns a string body into a null field, so a string-bodied reply silently loses its content at that hop. Diagnosis recipe: (1) read the forwarding path, not the encode helpers - the truth is the bytes handed to the connection write; (2) prove it live with two peers on a scratch port, replying once with an object and once with a string, and assert the received Python/JSON type matches what the responder sent (value_identical plus same type); (3) for a served doc, run the same probe against a freshly built binary at HEAD before editing prose, so the doc is aligned to measured behavior rather than to a recalled belief. Fix: state the wire truth in the doc's field table ('any | opaque JSON value, relayed verbatim - the responder decides the JSON type'), keep the object example, document the map-decode consequence for the bridge, and pin it with a regression test whose fixtures are deliberately non-canonical (unsorted keys, loose whitespace, < and &) so that json.Marshal of the decoded value does NOT reproduce the sent bytes - that makes a server-side re-encode fail the byte-identity assertion. Non-vacuity proof: mutate the forward call to json.Unmarshal+json.Marshal the frame and require the new subtests to FAIL, then restore and assert the source file's sha256 matches the pre-mutation value.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-json-rawmessage-field-relayed-verbatim-not-stringified", "provider": "openrouter", "solved_at": "2026-09-18T17:30:42.548Z", "version": ""}