Owner ruling 2026-09-25. runPiTurn executes a turn decoupled from any HTTP
response: an unbounded memory-only event log (finite by construction; the
30-min liveness TTL bounds the pathological case) plus a subscriber set.
The POST /api/chat stream and the new GET /api/chats/[id]/stream are both
subscribers — identical SSE dialect; late subscribers get FULL replay
(starting at the start chunk) then live until finish + [DONE]. A view
cancelling (reload, navigate) now detaches ONLY — never aborts the turn.
The remote kill is the abort channel: POST /api/activity/abort gains
kind: "turn" (id = chatId; not_running 409 like jobs); handle.abort sets
sawAbort + agent.abort(), every subscriber closes cleanly, the partial
persists through the usual settle (onFinish runs once, then endTurn,
then unregisterTurn LAST so a view that finds nothing finds the record
current). No running turn on reattach: the cheap terminal — one
{"type":"turn-settled"} event + [DONE] (settledTurnResponse).
The live smoke caught what the unit suite had masked: the agent_end
close() had been left behind in the restructure — ReferenceError every
turn, steered into the error path; fixed and pinned (clean turns must
never emit an error chunk; finishReason stop present).
22 lines
1.0 KiB
TypeScript
22 lines
1.0 KiB
TypeScript
/**
|
|
* THE REATTACH STREAM (background-turn runner wave, owner ruling
|
|
* 2026-09-25): one SSE stream per chat, same wire dialect as the POST
|
|
* /api/chat stream. 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);
|
|
}
|