Root cause (postmortem, verified live): Chromium caps 6 concurrent
HTTP/1.1 connections per origin. Every running turn pinned slots for
its whole life — the POST /api/chat response stream plus a reattach
GET stream per visit that never closed on navigate-away — so two
concurrent thinking sessions starved navigation RSC fetches in the
browser queue (147s measured) until the turns settled. Server was
exonerated (3ms curls during the freeze); main thread idle throughout.
- COMPACTION LAW + WIRE DIET (ui-stream): store the states, not the
chunks — replace-in-place snapshot retention keyed by updateSlots,
~100ms coalesced latest-wins fan-out, slim wire drops pi's duplicate
assistantMessageEvent payload.
- DIET B: POST /api/chat acks and closes ({ok:true}) — the turn wire
is GET /stream for everyone, submitter included; send() reattaches
via refetch-first resumeStream(true).
- DIET A: unmount cleanup calls model.stop() — navigating away aborts
the view connection (turns are server-side background objects).
- Answer-flicker fix: resumeStream's eager tail clear moved off the
refetch path (the refetch swaps base+tail atomically; clean-slate
guard restored on refetch failure) — the optimistic answer card
survives refetch latency (1.7s stalls measured on dev).
- Tests converted to the ack contract (drainTurn/drainRunning helpers,
dietFetch mock) + a pin that the POST body reads while the turn runs.
Gates: 1489/1489, typecheck, lint. Live: dual streaming sessions,
navigations 335-648ms, in-page settings 19ms (was 147s).
24 lines
1.1 KiB
TypeScript
24 lines
1.1 KiB
TypeScript
/**
|
|
* THE REATTACH STREAM (background-turn runner wave, owner ruling
|
|
* 2026-09-25) — and, since CONNECTION DIET B (owner, 2026-10-10), THE
|
|
* turn wire, period: POST /api/chat acknowledges and closes, so the
|
|
* submitter too attaches here. One SSE stream per chat: a running turn →
|
|
* subscribe to the live turn object: FULL replay of the event log, then
|
|
* live chunks until settle ([DONE]). No running turn → the cheap terminal
|
|
* ({"type":"turn-settled"} + [DONE]): the record is current, the client
|
|
* refetches it.
|
|
*
|
|
* 404 mirrors the POST handler's unknown-chat. Framework route is a thin
|
|
* wrapper over the server barrel (the import law); tests drive the seams
|
|
* (turn-runs + ui-stream) directly.
|
|
*/
|
|
import { getChat, runningTurn, piTurnResponse, settledTurnResponse } from "@/src/server";
|
|
|
|
export async function GET(_req: Request, ctx: { params: Promise<{ id: string }> }): Promise<Response> {
|
|
const { id } = await ctx.params;
|
|
if (!getChat(id)) return Response.json({ error: "Unknown chat" }, { status: 404 });
|
|
const handle = runningTurn(id);
|
|
if (!handle) return settledTurnResponse();
|
|
return piTurnResponse(handle);
|
|
}
|