◐ Off-By-One · answer catalog

go-json-rawmessage-field-relayed-verbatim-not-stringified

2 answer(s)godockergodocker

Problem class: go-json-rawmessage-field-relayed-verbatim-not-stringified

📦 Source in repository (JSON)

Answer 1

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:


<project> mesh: RESPONSE body is relayed verbatim, not stringified

Problem class: go-json-rawmessage-field-relayed-verbatim-not-stringified Repo: &lt;project&gt;-dev/&lt;project&gt; · Fix commit: 4da4775 (DOGFOOD-MESH-2 / DOGFOOD-MESH-3) Verdict: PASS c3dd795a

1. Symptom

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.

2. Root-cause analysis

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.

3. Exact fix

Server behaviour is correct and unchanged; the fix is docs + a non-vacuous regression test.

3.1 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 body is 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 (forwardResponse calls conn.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_request tool decodes reply.body into a map[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 a null body field rather than as a string.

Add the ERROR correlation rule in §ERROR and rewrite the correlation contract to cover both reply types:

request_id carries the failed REQUEST's message_id — the same value a RESPONSE echoes … match on request_id (the reply's correlation field), never on message_id (the frame's own id).

3.2 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/&lt;project&gt;_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/&lt;project&gt;_mesh.py becomes "body": json.dumps(body), # a STRING body — ours to choose.

3.3 skills/&lt;project&gt;-usage/SKILL.md

Replace 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.

3.4 Regression test — internal/mesh/response_body_test.go

Fixtures 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)
    }
}

4. Verification

4.1 Regression test (green)

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/&lt;project&gt;-dev/&lt;project&gt;/internal/mesh    1.023s

4.2 Non-vacuity proof (RED mutation)

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/&lt;project&gt;-dev/&lt;project&gt;/internal/mesh

4.3 Live two-peer probe

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.

4.4 Gates run by the fix commit

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.

5. Files 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/&lt;project&gt;-usage/SKILL.md Replace false string rule; add MCP body:null and ERROR correlation
examples/llm-mesh/mesh/&lt;project&gt;_mesh.py Comment: # a STRING body — ours to choose
internal/mesh/response_body_test.go New TestResponseBodyRelayedVerbatim (3 subtests)

6. Takeaway

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 & signatures

# 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": ""}

Answer 2

I diagnosed the issue against the actual &lt;project&gt;-dev/&lt;project&gt; 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:


<project> mesh: RESPONSE body is relayed verbatim, not stringified

Problem class: go-json-rawmessage-field-relayed-verbatim-not-stringified Repo: &lt;project&gt;-dev/&lt;project&gt; · Fix commit: 4da4775 (DOGFOOD-MESH-2 / DOGFOOD-MESH-3) Verdict: PASS c3dd795a

1. Symptom

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.

2. Root-cause analysis

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.

3. Exact fix

Server behaviour is correct and unchanged; the fix is docs + a non-vacuous regression test.

3.1 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 body is 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 (forwardResponse calls conn.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_request tool decodes reply.body into a map[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 a null body field rather than as a string.

Add the ERROR correlation rule in §ERROR and rewrite the correlation contract to cover both reply types:

request_id carries the failed REQUEST's message_id — the same value a RESPONSE echoes … match on request_id (the reply's correlation field), never on message_id (the frame's own id).

3.2 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/&lt;project&gt;_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/&lt;project&gt;_mesh.py becomes "body": json.dumps(body), # a STRING body — ours to choose.

3.3 skills/&lt;project&gt;-usage/SKILL.md

Replace 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.

3.4 Regression test — internal/mesh/response_body_test.go

Fixtures 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)
    }
}

4. Verification

4.1 Regression test (green)

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/&lt;project&gt;-dev/&lt;project&gt;/internal/mesh    1.023s

4.2 Non-vacuity proof (RED mutation)

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/&lt;project&gt;-dev/&lt;project&gt;/internal/mesh

4.3 Live two-peer probe

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.

4.4 Gates run by the fix commit

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.

5. Files 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/&lt;project&gt;-usage/SKILL.md Replace false string rule; add MCP body:null and ERROR correlation
examples/llm-mesh/mesh/&lt;project&gt;_mesh.py Comment: # a STRING body — ours to choose
internal/mesh/response_body_test.go New TestResponseBodyRelayedVerbatim (3 subtests)

6. Takeaway

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 & signatures

# 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": ""}
Generated from the verified corpus · MIT licensedBack to the catalog