Dynamic execution enginestreamingRedis PubSubdiagnosis

Why a channel key should not be the request ID

Naming the Redis channel after the request ID gave a nested call two subscribers. Five tokens arrived as 195, and the same cause came back weeks later on parallel calls.

Mar 13, 20265 min

Code written by a user runs inside an agent, and that code can call another agent. When it does, the answer comes back token by token, and those tokens travel on to the browser.

I ran a single line: print("Hello World"). The model produced five tokens — print, (", Hello, World, "). The browser received 195 SSE events.

Two numbers from the same run
SSE events the browser received
195
Tokens the model actually produced
5

Nothing was wrong with the content — the same thing kept coming. The same sentence stacked up on screen dozens of times.

The channel was named after the request ID

When the chat service takes a request, it subscribes to a Redis channel named sse.response:{request ID} and pushes whatever appears there to the browser. That is the SSE response.

The sandbox publishes to the same channel directly, so that progress messages like "generating code…" reach the browser. So far this is the design.

The problem is that the sandbox passed its request ID down unchanged when it called another agent. With the same request ID the channel name comes out the same, and the second chat service, the one handling that agent, ends up subscribing to the very same channel.

BrowserChat service ASandboxChat service Bqueryrun codecall another agent · same request IDtoken "print"reads back what it wrotethe same token, over and over
1 / 6

01A subscribes to sse.response:ORIG. Correct so far.

The order the tokens multiplied in

The order in the logs shows the mechanism directly. The first token inflates on its own, and from there the whole run of tokens so far circles as one block.

StageWhat appeared on the channel
1print × 7
2(" + 1 earlier token
3Hello + 2 earlier tokens
4 World + 3 earlier tokens
5") + 4 earlier tokens, the 5-token block repeating 38 times

By its symptom this is a streaming bug, so the streaming code is where I looked first. I went through buffering, chunk boundaries and reassembly, and none of it was the cause. There were two services listening on the channel, so no amount of looking at the pipe was going to turn anything up.

Tokens mixed across parallel calls

Weeks later a different symptom arrived. This time it was parallel calls: code in the sandbox calling two agents at once for the weather in Seoul and in Los Angeles.

Los Angeles came back clean. Seoul came back like this.

## 🌤# 🌤️# 🌤️# 🌤️ Tomorrow# 🌤️ Tomorrow#'s Weather 🌤️ Tomorrow#'s
Weather 🌤️ in Tomorrow Seoul#'s Weather 🌤 (️Web Search in Tomorrow Seoul#'s …

This time the tokens were ending up in each other's positions instead of repeating. While streaming, a citation goes out as a placeholder first, and once the source is resolved an event follows saying "replace this string with that one". Two parallel requests shared one channel, so those replacements landed in each other's text. They applied where a replacement had already happened, and partial matches accumulated.

The symptoms looked nothing alike, but the cause was the same. The channel was named after the request ID, and the request ID wasn't unique per stream.

Giving the inner call its own request ID

The first thing shipped was a stopgap. Pubsub publishing was moved out of the agent level and up to the orchestrator level, which took away the publisher on the inside, and without it the loop can't close. The symptom stopped, but the channel name stayed as it was.

After that came the fix that derives a new ID for the inner call.

# A fresh ID for the inner call — the channels split
inner_request_id = uuid7str()
inner_headers = {
    headers,
    "x-request-id": inner_request_id,
    "x-request-traces": f"{x_request_id},{inner_request_id}",
}

The first value in x-request-traces stays the original. The publish target is taken from that first value, so the channels split while the path out to the browser stays the single one it always was. The trace chain survives too.

The parallel case ended the same shape. When the shared parallel client builds per-task headers, it derives the request ID as {original}-{call name}-{8 random chars}. Read the name and you can see which original it branched from and which call it is.

The receiving side was left alone

What got fixed was the caller. The chat service on the receiving end fills in its own name when the originating-agent header is missing, and on a nested call an empty value is almost certainly a mistake, yet it neither rejects it nor warns. It just carries on.

The file behind this incident is user code, one per agent, so it has no contract and lives outside version control. It forwards the agent-name headers but passes the original request ID straight through. The shared client handles all thirteen headers for you, and nothing blocks the path around it that hand-rolls an HTTP client. I don't think the gap is really knowledge; it's that the bypass is open.

Logging request ID, originating pod and timestamp just before each publish would have made "there are two subscribers" visible on day one. The post-mortem says as much. A test where an agent calls an agent and the browser's events get counted would probably have caught the parallel case ahead of time too, but once the nested-call incident closed that check slipped down the list, and weeks later I ran into the same problem on parallel calls.

End