import { AssistantConversationStatus } from './conversation'; /** * Is a turn genuinely in flight for this conversation? * * One predicate, three callers, because they must not drift: the re-entry guard * in `handleSend`, the `aria-busy` / streaming-message derivation, and the * composer's disabled state. The subtlety it encodes is that a processing * ENTRY outliving the turn is normal — `setConversationProcessing` keeps the * conversation keyed as 'complete' or 'error' afterwards — so "has an entry" is * not the question; "is it one of the live statuses" is. * * Type-only import, so this module stays importable in the node-environment * test suite. */ export declare function isTurnInFlight(status: AssistantConversationStatus | undefined): boolean; /** * Should this turn's own runtime give up, and may it clear the shared state? * * ★★ Cancelling the model is only half of cancelling a turn. The client-side * poll loop that drives a turn keeps running after the abort: it goes on * patching the cancelled message with whatever the sandbox produced, and on * exit writes `setConversationProcessing(id, 'complete', )`. * * That write is the damaging one. `processingByConversationId` holds ONE entry * per conversation, so a turn submitted in the meantime already owns it — and * the abandoned turn overwrites it with a completion for the answer the user * cancelled, leaving the live turn's own completion landing on an entry that no * longer describes it. Silently: there is no error on either side. * * ★ Scope. This now fires in the VOICE flow: the orb is pressable during * `thinking` and `micIsInterrupt` covers it, so a user can cut off an answer * while it is still being generated. It also covers the host-page * `pendingPrompt` route, which starts turns past the voice overlay. * * Two earlier versions of this comment were wrong in opposite directions, which * is why the scope is now stated rather than assumed. The first claimed this * guard stopped the cancelled answer being read aloud — it never could. The * second said it could not fire from voice at all — true only while the orb was * inert during `thinking`, which was the very thing this work removed. * * ★★ It is still NOT what delivers stale-output rejection. Old TEXT reaching a * new caption is a separate defect with a separate fix — see `publishedChunks` * in `voice/speechPlayback.ts`. Do not conflate them. * * ★ `clearProcessing` is addressed the same way `planTurnAbort` is. An * abandoned turn may clear the shared entry only while that entry is still its * own — otherwise it would strand the very answer the user is now waiting for. * When nothing replaced it, clearing is what stops the composer sitting * disabled forever behind a turn that will never complete. */ export declare function planAbandonedTurn(args: { /** The message id this runtime is driving. */ turnId: string; /** A cancellation has been requested for it. */ abortRequested: boolean; /** The turn the shared processing entry currently describes. */ activeTurnId: string | null | undefined; }): { abandon: boolean; clearProcessing: boolean; }; /** Why an abort was or was not sent. Carried so callers can log the near-miss. */ export type TurnAbortReason = "send" | "nothing-running" | "superseded" | "already-requested"; /** * Should this cancellation actually be sent to the sandbox? * * ★★ THE HAZARD THIS EXISTS FOR. `POST /sessions/:id/abort` is scoped to the * SESSION, not to a turn — and the session is reused for the whole * conversation. So the endpoint does not cancel "the turn you interrupted", it * cancels *whatever is running in that session at the moment it arrives*. * * That makes a late abort actively dangerous rather than merely useless: the * user interrupts turn A, the request is slow, the user asks B, and the abort * lands on B — killing the answer they are actually waiting for, with no error * anywhere. The failure looks like the assistant randomly giving up. * * So the abort is addressed to a turn and refused unless that turn is still the * one in flight. Pure, and separated from the request itself, because the rule * is a safety property and the hook that would otherwise hold it has no test * file — which is exactly how `planReplyConsumption`'s original defect shipped. * * This is a guard, not a guarantee: it closes the window this process can see. * A turn that starts on ANOTHER device between the check and the request is not * addressed here and would need a server-validated turn id. */ export declare function planTurnAbort(args: { /** The turn the user asked to cancel. */ interruptedTurnId: string | null | undefined; /** The turn actually in flight right now, or null if none is. */ activeTurnId: string | null | undefined; /** An abort for this same turn has already been sent. */ alreadyRequested?: boolean; }): { send: boolean; reason: TurnAbortReason; }; //# sourceMappingURL=turnState.d.ts.map