Story-1266 · pramaan-docs ADR-110 (single WS rail, agent_event_stream.v1)

One header was welding the stream shut

The problem

Atlas's Step-5 review was supposed to type out live — the matter description streaming token-by-token over the ~2s generation window. Instead it arrived in 1–2 jumps. Every layer was already stream-capable, which made the bug counterintuitive: nobody was obviously buffering.

Why it bunched — the layer-by-layer trace

LayerState
Brain (app/atlas/runtime.py, pramaan-agents)Already emits a turn.delta per BAML partial — granular, one yield per token-ish chunk.
AgentCore RuntimeAlready SSE-frames each yielded line as data: "<json>" (Bug-1229). But it only flushes those frames incrementally when the caller negotiates streaming; otherwise it collects the entire generator and returns one blob at the end.
Broker (src/clerk_worker/runner.py, this repo)Reads body.iter_chunks(4096), splits on newlines, unwraps data:, and pushes per parsed delta. Forwards data the instant it arrives — never waits to fill a buffer.
App (pramaan-app)Renders every delta the moment it lands — verified live, caught the description mid-sentence.

So three of four layers were stream-correct and the broker read was instant. The bunching was upstream of the broker read: the runtime handed the broker the whole stream at once, because of how the broker asked for it.

The root cause

The broker invoked AgentCore with accept="application/json". Per the AgentCore InvokeAgentRuntime contract, application/json means buffer the entire generator and return one JSON blob when it finishes. SSE chunks flush incrementally only when the caller sends Accept: text/event-stream. The brain's per-token granularity and the runtime's SSE framing were both intact — they were just being collected behind a buffer the broker itself had requested.

The fix

PieceChange
_invoke_brainaccept="application/json" → accept="text/event-stream" on invoke_agent_runtime. One line. The runtime now flushes each yielded frame as the brain produces it.
Per-line push / data: unwrapUnchanged. The iter_chunks + newline-split + data: unwrap already handle SSE framing (that framing was always present — Bug-1229). The v1 frame shape is untouched.
Regression lockThe happy-path unit test now asserts the broker negotiates text/event-stream, so a future revert to application/json fails CI instead of silently re-welding the stream.
Fully functions-side. No pramaan-agents or agentcore config change was needed — the brain yields per-partial and the runtime already SSE-frames; the accept header was the only lever suppressing incremental delivery (satisfies ADR-031, single-repo executable change). WebSocket rail only; the legacy SSE path (router_agents.py, retired by ADR-110) was not touched.

Verification

ruff check src/clerk_worker/runner.py tests/.../test_runner.py  -> clean
pytest tests/unit/clerk_worker/test_runner.py                  -> 10 passed
  (incl. new assertion: invocation accept == "text/event-stream")
No route/DTO/router change -> no OpenAPI snapshot impact.
System-boundary (live) verification is the acceptance step and runs against the deployed brain + WS rail: confirm the Step-5 description types out across the ~2s generation window rather than in 1–2 jumps. The unit lock plus the documented runtime contract establish the root cause; the browser-visible confirmation is pending deploy.