/** * `TurnHandle` — one turn's view of the fold (spec 14990 FR-018, INV-014; * tasks.md T030). * * A handle owns two things: the SS3.1.4 turn-wait, and the async iterators * that ride the fold for this turn. It authors no wire traffic and holds no * durable state — `Session` feeds it folded events and it fans them out. * * INV-006 governs the wait: the SDK never locally times out, fails, or * completes a turn. Every settlement below traces to a server-authored fact, * with one sanctioned exception that is not a terminal at all — * terminal-unknown after an ephemeral host death, which SS2.13.3b makes a * client MUST ("stop waiting for a terminal") and which stays an annotation * rather than a synthesized event. */ import type { Item, ItemDeltaParams, TurnCompletedParams, TurnUnqueuedParams } from "../msp.js"; import type { DeepReadonly } from "../fold/session-fold.js"; /** * What the iterators yield: the fold's stored item, sealed. * * `DeepReadonly` because these ARE the objects the fold holds — the generated * wire types carry no `readonly`, so an unsealed yield would let a consumer * write `item.revision = 0` and defeat the INV-003 guard through the read * surface (PR #23087 review round). The seal is compile-time only; nothing is * frozen or cloned on the hot path. */ export type FoldedItem = DeepReadonly; /** * How a turn ended, from a waiter's point of view. * * SS3.1.4 names exactly two resolutions of the turn-wait, and the pre-minted * turn is why there are two rather than one: * * - `completed` — the ordinary terminal, AND the launch-failure exit * (`"deferred_start_failed"`: terminal `"failed"`, `error.kind: * "launchError"`, no preceding `turn/started`). `isLaunchFailure` reads * that WIRE marker; `observedStart` does not discriminate them, because a * terminal-first fold is legitimate for turns that really ran. * - `unqueued` — the reclaim (SS3.6). No `turn/completed` will ever carry * this `turnId`, so a wait folding only `turn/completed` hangs forever. * * `turn/retracted` and `turn/retryScheduled` are deliberately absent: a * retracted turn still reaches its own terminal, and a scheduled retry is * explicitly non-terminal (SS4.5.1). */ export type TurnOutcome = { readonly kind: "completed"; readonly params: TurnCompletedParams; /** * Did THIS session observe a `turn/started` for this turn? * * A session-local observation, NOT a launch-failure marker. The fold * deliberately accepts a terminal-first `turn/completed` for turns that * really did run — single-shot turns, gap fills, and any consumer that * attached mid-stream or replayed a canned transcript from partway * through — so `false` here covers those too. * * The launch failure has its own WIRE marker and that is the one to * branch on: `terminal: "failed"` with `error.kind: "launchError"` * (tdd SS3.1.4/SS4.5.1). `isLaunchFailure` below reads it. */ readonly observedStart: boolean; } | { readonly kind: "unqueued"; readonly params: TurnUnqueuedParams; } /** * SS2.13.3b: an ephemeral host died, so the client stops waiting. Carries no * `reason`: the arm has exactly one cause, so a single-valued field would * add nothing `kind` does not already say while freezing a permanent * obligation into a public type (Constitution XI). */ | { readonly kind: "terminalUnknown"; }; /** * The `"deferred_start_failed"` exit (SS3.1.4/SS3.2): a pre-minted turn whose * launch errored at the terminal boundary, so the runtime wrote its terminal * directly and no `turn/started` was ever emitted BY THE HOST. * * Read off the wire, never off local observation: a client that had not been * listening when the turn started is not looking at a launch failure. */ export declare function isLaunchFailure(outcome: TurnOutcome): boolean; /** The turn's replayable backlog, supplied by `Session` from the fold. */ export type ItemReplay = () => readonly FoldedItem[]; /** * A turn, as a CONSUMER sees it — the SS7.1 turn surface and nothing else. * * `Session.turn()` returns this rather than the concrete `TurnHandle` because * the handle's "fed by Session" mutators (`settleCompleted`, `settleUnqueued`, * `fail`, `pushItem`, …) would otherwise be public: an embedder could call * `settleCompleted` and fabricate a terminal the server never authored, which * is exactly what INV-006 forbids. They stay reachable only through * `Session.apply`, i.e. only from a server-authored event. */ export interface Turn { readonly turnId: string; /** Did THIS session observe a `turn/started`? Not a launch-failure marker. */ readonly observedStart: boolean; readonly completed: Promise; items(): AsyncIterableIterator; deltas(): AsyncIterableIterator; } export declare class TurnHandle implements Turn { #private; readonly turnId: string; constructor(turnId: string, replayItems: ItemReplay); /** Settles on `turn/completed`, `turn/unqueued`, or the ephemeral discard. */ get completed(): Promise; /** * How many iterators this handle is still fanning out to. * * Exposed so the deregistration contract is assertable: an iterator a * consumer broke out of, or simply dropped, must leave this set rather than * accumulate for the rest of the turn. */ get liveStreamCount(): number; /** Did a `turn/started` ever fold for this turn? */ get observedStart(): boolean; /** * This turn's items: everything the fold already holds for it, at its * current revision and in first-opened order, then the live tail — one yield * per fold-observed revision CHANGE. A stale re-emission mutates nothing * (INV-003) and therefore yields nothing. * * The iterator ends when the turn settles; it never ends on a local timeout. */ items(): AsyncIterableIterator; /** * This turn's `item/delta` frames, live only. * * Deliberately NOT replayed, and the asymmetry with `items()` is a fact * about the fold rather than an oversight: the store holds each item at its * latest revision plus the ACCUMULATED delta text per field path * (`ItemStore.accumulated`), never the delta event sequence. A consumer * attaching mid-turn reads the accumulated value from the fold; there is no * retained event list to replay, and inventing one would be a second copy of * state that INV-002's determinism would then have to defend. */ deltas(): AsyncIterableIterator; markStarted(): void; pushItem(item: FoldedItem): void; pushDelta(params: ItemDeltaParams): void; settleCompleted(params: TurnCompletedParams): void; settleUnqueued(params: TurnUnqueuedParams): void; /** SS2.13.3b: an ephemeral host died; stop waiting for a terminal. */ settleTerminalUnknown(): void; /** * A DURABLE host died abnormally. The waiter gets no answer here — FM-001 * puts the terminals on resume — so it is rejected rather than resolved: a * resolution would be the invented terminal INV-006 forbids, and silence * would be the SS3.1.4 hang. */ fail(error: unknown): void; }