/** ENG-353 — server-side turn liveness: the idle-abort fallback for client * disconnects the runtime never surfaces. * * The fast path is unchanged: `request.signal` cancels the turn the moment the * runtime propagates a fetch abort (wave-5 AGENT-3). But under `next dev` a * real browser's graceful tab-close/navigate-away fires neither the signal nor * a stream cancel, so an abandoned turn runs to completion. The fallback is * liveness by heartbeat: the panel beats `POST /threads/:id/heartbeat` while * it consumes the stream (08 — `withTurnHeartbeat`); the FIRST beat arms the * watchdog, and from then on `IDLE_ABORT_MS` of silence aborts the turn. * Arming is opt-in by construction: consumers that never beat (curl drills, * scripted clients, older panels) keep run-to-completion semantics. * * The registry lives on globalThis (Symbol.for) so HMR copies of this module * under a dev server share one view: a turn registered before an edit is still * beatable after it. */ /** Track one streaming turn; returns its unregister. Registration alone never * arms the watchdog — only a first heartbeat does. */ export declare function registerActiveTurn(turn: { threadId: string; subject: string; turnId?: string; abort: () => void; }): () => void; /** A heartbeat for `threadId` from `subject`. Refreshes (and on first beat * arms) the idle watchdog of every matching in-flight turn. Foreign or * unknown ids answer false — no oracle, and a beat can never keep (or end) * another principal's turn. */ export declare function touchActiveTurn(threadId: string, subject: string): boolean; /** * The turn's thinker is done: from here it is only the turn's own closing work — * the workspace commit that collects and syncs back what the agent built, then * the transcript, the harness state and the audit row. The watchdog stands down * at exactly this line. * * It exists because those two phases have opposite answers to "the client * vanished". A client that leaves MID-STREAM is what the watchdog is for: nobody * is waiting for the tokens still being generated. A client that leaves after * the last token is not — the work is already done and paid for, and aborting it * loses what the turn just made. That is the shipped failure: an abort landed * during sync-back, the turn's app never reached the store, and the response was * still a 200. * * Published from INSIDE the turn (`liveTurn`'s disposer, `harness-turn.ts`) for * the same reason the steer sink is: the boundary is a moment in the runtime's * loop, and the wire cannot see it — the response body stays open through the * whole commit, so the bytes running out is far too late to mean this. * * Addressed by TURN and not by thread, unlike the beat: nothing serializes two * turns on one thread, and standing one turn's watchdog down must not stand its * sibling's down with it — that sibling is still streaming, and reaping it if * its client leaves is the whole point of the watchdog. * * The registration STAYS: the turn really is still in flight, and a beat should * keep saying so until its stream ends. */ export declare function finishActiveTurn(turnId: string): void; /** * The mid-turn STEER sink of a turn in flight (§10.2). * * Its own registry rather than a field on {@link ActiveTurn} because the two are * published by different halves at different moments: the wire registers the * abort from OUTSIDE the turn, once `runTurn.stream` has returned, while the * runtime publishes this from INSIDE it (`liveTurn`). One entry with two * registrars would be a two-phase handshake for no gain. */ interface SteerableTurn { threadId: string; subject: string; steer: (text: string, messageId: string) => Promise; } /** Publish this turn's steer sink; returns its retraction. */ export declare function registerTurnSteer(turn: SteerableTurn): () => void; /** * Hand `text` to `subject`'s own turn in flight on `threadId`. Principal-scoped * exactly like {@link touchActiveTurn}: foreign or unknown ids answer `false` — * no oracle, and nobody can speak into another principal's build. * * `false` is a FACT and not a failure: it is also the answer when the turn simply * cannot take a message, and the caller's own queue is the fallback either way. */ export declare function steerActiveTurn(threadId: string, subject: string, text: string, messageId: string): Promise; /** Wrap a turn response so `onSettled` runs exactly once when its stream * finishes, errors, or is cancelled — the turn's registry entry must not * outlive the stream. Mirrors the wire's inflight-bracket wrapper. * * `idle` is the watchdog's own abort (the wire holds one per turn, separate * from the client-disconnect fast path): when it fires, this client's stream * ENDS — terminal chunks, then close — because a turn the server ended must * read as an ending on the wire. Only this branch is ended; the recording * branch underneath keeps following the real turn, so a client that rejoins * through `GET /threads/:id/stream` still replays what actually happened. */ export declare function trackTurnResponse(response: Response, onSettled: () => void, idle: AbortSignal): Response; export {};