A TurnStream in QM tracks per-run state across an agent's full reply lifecycle—from start through block publishing, tool calls, and final confirmation—and fires callbacks when the first text block closes or the reply posts to a surface. Character limits (overall maxChars and first-block cap) and a grace-period timer prevent unbounded buffering while allowing the Node.js process to exit cleanly.
The TurnStream interface in src/runs/turn-stream.ts tracks per-run streaming state across the full lifecycle of an agent reply: from begin() through publish(), publishBlockStart(), and noteToolCall(), to markReplyDone() and end(). The TurnStreamListener interface in the same file exposes two optional callbacks: onFirstBlock(text), fired when the first text block is closed by a tool call, and onSurfacePosted(), fired when the reply is confirmed posted to a surface.
createTurnStream() is the factory for a TurnStream instance; it accepts optional maxChars (default 200,000) and graceMs (default 30,000 ms) parameters. The first text block in a run is additionally capped at 20,000 characters (FIRST_BLOCK_MAX_CHARS), independently of the overall maxChars limit. The grace-period timer calls timer.unref?.() so it does not prevent the Node.js process from exiting when a run-map entry is the only remaining work. The grace-period timer starts when a run ends; if the TurnStream entry is not explicitly cleared within the grace window, it is removed automatically to prevent memory leaks from replies that never receive final confirmation.
The web-UI chat surface uses z-index / elevation layers (defined in plugins/web-ui/src/shell.css and chat.ts) to keep the composer and sticky prompt floating above scrolling response content. The sticky prompt positioning in transcript-viewport.ts is coordinated to prevent layout thrash when the viewport auto-follows new tokens during streaming. Live thinking blocks are preserved in the visible transcript during streaming; chat.ts ensures they are not dropped when content streams.