/** * `Session` — the SS7.1 facade's composition of the transport-less core * (spec 14990 FR-018 / T030, FM-002 / T030b). * * It owns a `SessionFold` and a `PendingCommandSet`, routes folded view events * to per-turn handles, and discharges the ephemeral host-death obligation. * Like everything else in this SDK it holds no durable state (spec Lifecycle & * Composition): every fact it reports came from a server event or from the two * stores it composes. * * The SS4.8 splice-fill (FR-020, T032) is here too, in `gap-fill.ts`: this * layer owns it because the recipe needs client→server I/O (`view/page`) and * because the buffer it splices is the same routing path the iterators read. * * Everything else FR-018/FR-019 name IS here as of #23980: `sendUserTurn` over * the enrolled `turn/start` row, `onApproval`'s idempotent `approval/decide` * submission, and the SS4.13 retirement drivers that need client->server I/O. * The connection is an OPTIONAL seam: a fold-only `Session` — the transport-less * composition every earlier arm builds — stays valid and refuses the submit * verbs loudly rather than dropping a caller's input on the floor. */ import { SessionFold } from "../fold/session-fold.js"; import type { FoldOutcome, ViewEvent } from "../fold/session-fold.js"; import { PendingCommandSet } from "../pending/pending-command-set.js"; import type { PendingRetirement, ReplayAnswer, SnapshotJoinFacts } from "../pending/pending-command-set.js"; import type { Connection } from "../connection/connection.js"; import type { ApprovalFailureHandler, ApprovalHandler } from "./approval.js"; import type { SendUserTurnOptions } from "./turn-submit.js"; import type { GapFillFailureHandler } from "./gap-fill.js"; import type { DiscardedSessions } from "./discarded.js"; import type { HostDeathDischarge, HostDeathNotification, SessionDurabilityProfile } from "./host-death.js"; import type { Turn } from "./turn-handle.js"; import type { SessionResumeResult, SessionStartResult, UserInputAnswer, UserInputAnswerResult } from "../msp.js"; /** * Which verb opened this session, and what it answered. * * `MuseClient.startSession`/`resumeSession` resolve to a `Session` (FR-018), so * without this the typed wire result — `viewCursor`, the resume history, the * server's own `sessionId` — would be swallowed by the wrapper. Discriminated * by the verb rather than a bare union, so a consumer that only handles resume * cannot silently read a start result's absent members. */ export type SessionOpening = { readonly verb: "session/start"; readonly result: SessionStartResult; } | { readonly verb: "session/resume"; readonly result: SessionResumeResult; }; /** * Options for constructing a `Session` directly. Sessions opened through * `MuseClient` are configured for you; build one yourself when you manage * the connection — or omit the connection to fold recorded events with no * host at all. */ export interface SessionOptions { readonly sessionId: string; /** * Required, not defaulted. Whether this session survives its host decides * what happens to every in-flight item and command when the host dies * (SS2.13.3b), and a default would let a caller skip reading the handshake * and silently inherit the wrong obligation. `readSessionDurability` turns * an `InitializeResult` into this value. */ readonly durability: SessionDurabilityProfile; /** * The wire seam the submit verbs author through. OPTIONAL because fold-only * construction is a real current use, not a hypothetical one: every * transport-less arm in this package builds a `Session` with no host at all. */ readonly connection?: Connection; /** * What an ephemeral host death already discarded, shared across the sessions * ONE client opened (SS2.13.3b, T030 obligation (c)). Omitted, this session * remembers only its own discards — the pre-#23980 behaviour. */ readonly discarded?: DiscardedSessions; /** Set by `MuseClient`; see {@link SessionOpening}. */ readonly opening?: SessionOpening; } /** * The fold as a consumer may READ it. * * An ALLOWLIST (`Pick`), not `Omit` of the mutator names. `Omit` never checks * its keys against the type, so every future public `SessionFold` method would * land on this barrel-exported "read-only" view silently — and the contract * already plans one: `seedFromSnapshot` (FR-008), a mutator that would feed the * fold past `apply`'s turn routing and discard latch. `Pick` constrains its * keys to `keyof SessionFold`, so both drift directions are compile errors. * This is the same conversion `FoldItems` made one layer down (PR #23087). */ export type SessionFoldView = Pick; /** * The pending set as a consumer may READ and DRIVE it. * * `discardEphemeral`, `observedReclaim` and `observedUserMessage` are * `Session`'s to call: it drives them from folded events and reports what they * returned. A consumer calling `discardEphemeral()` directly drains the set and * latches it discarded, so the later real `hostExited` reports * `retiredCommands: []` — silently losing the very inputs the discharge exists * to hand back. Same allowlist discipline as the fold view. * * `stopRetrying` and `replayAnswered` are hidden for the same bookkeeping * reason, re-exposed as `Session` methods: both can retire an entry the * submit verb authored, and a retirement that bypasses * `TurnSubmitter.forgetRetired` leaves that command's `turn/start` params in * the replay memory forever — unbounded over the sanctioned SS3.1.1 * submit-then-abandon flow. */ export type PendingCommandView = Pick, "ackErrored" | "acked" | "discarded" | "get" | "has" | "joinSnapshot" | "list" | "observedQueueMovement" | "reconnectedWithoutSnapshot" | "resolveReplayAtJoin" | "size" | "submitted">; /** * `Session.apply`'s own refusal. NOT a `FoldOutcome`: the fold can never emit * it, and putting it there promised a direct `SessionFold` consumer a refusal * that seam does not produce. */ export interface SessionApplyRefusal { readonly kind: "refusedSessionDiscarded"; } /** * `Session.apply`'s other own verdict, for the same reason: a frame held while * an SS4.8 fill is in flight (FR-020 / T032). * * NOT a `FoldOutcome` — the fold has not seen this frame yet and reporting one * would state a fold move that has not happened. The frame is not lost: it is * re-fed through this same path, in cursor order behind the paged prefix, when * the fill splices. The pause a consumer sees is exactly this. */ export interface SessionGapBuffered { readonly kind: "bufferedDuringGap"; readonly method: string; } /** * A live frame the SS4.8 fill already applied from a page — the other half of * the recipe's overlap discard (PR #24930 review round 1). * * The wire promises no order between a `view/gap` and the frame at its `next`, * so the live twin of an event the page served can arrive after the fill * finished, when the buffer is gone. Refused once, by cursor equality, because * folding it again would re-run its routing — a duplicate SS4.13 queue-movement * replay on the wire, which is exactly what the discard exists to prevent. */ export interface SessionGapOverlap { readonly kind: "ignoredGapOverlap"; readonly method: string; readonly viewCursor: string; } /** * What one `Session.apply` did: the fold's own outcome, plus any SS4.13 * pending-command retirements the event triggered. * * Retirements ride the return value rather than a stream, matching the pull * model the pending set already uses: every * retirement is returned synchronously by the mutator that produced it — with * ONE stated exception. * * THE EXCEPTION (FR-020 / T032). A frame HELD during an SS4.8 fill reports * `bufferedDuringGap` with `retirements: []`, because at that moment nothing * has folded. Its real effects — an SS4.13 reclaim, an approval round trip — * happen when the fill splices, and they surface on the `io` of the EARLIER * `view/gap` apply that started the fill, never on the buffered frame's own * return. A consumer reconciling retirements frame by frame must therefore keep * the gap frame's outcome and await its `io`, or it will read `[]` for a * reclaim that really happened. * * A SECOND `view/gap` that lands while a fill is running rides the same * channel for the same reason: it extends the running walk instead of starting * its own, so its apply reports `deliveryGap` with an already-settled empty * `io`, and the work it caused settles on the FIRST gap frame's. A repeat of a * `next` already named while the hole is open folds as `ignoredStaleFrame` and * extends nothing; after a failed fill it still restarts that fill, and the * restarted walk's work settles on this frame's `io`. */ export interface SessionApplyOutcome { readonly fold: FoldOutcome | SessionApplyRefusal | SessionGapBuffered | SessionGapOverlap; readonly retirements: readonly PendingRetirement[]; /** * The client->server I/O this event triggered, settled — the SS4.13 replays * queue movement demands, and FR-019's `approval/decide` submission. Resolves * to the retirements that I/O produced. * * A SEPARATE channel from `retirements` because `apply` is synchronous by * contract: a consumer's notification pump must not be made to await a round * trip before it can fold the next frame. An event that triggers no I/O gets * a shared already-resolved empty, so awaiting this is always safe and never * allocates. * * IT NEVER REJECTS. A replay whose transport failed proves nothing about the * intake, so its entry holds and the authoritative report is the host-death * notification that is already on its way; an approval failure is reported to * `onApprovalError`. Rejecting here would surface as an unhandled rejection * in every consumer that does not await it — which is most of them, since the * value exists for tests and for consumers that want a barrier. */ readonly io: Promise[]>; } /** * One conversation with the agent. * * A session folds every server event into readable state — items, turns, * session facts, pending approvals — and submits your side of the exchange. * Get one from `MuseClient.startSession` or `resumeSession`, then * `sendUserTurn` to talk, `turn` to follow a turn's items to its outcome, * and `onApproval` to answer permission requests. */ export declare class Session { #private; readonly sessionId: string; /** See {@link SessionOpening}. Absent when the caller built this directly. */ readonly opening: SessionOpening | undefined; constructor(options: SessionOptions); get durability(): SessionDurabilityProfile; /** Read-only; events enter through `apply` so turn routing cannot be skipped. */ get fold(): SessionFoldView; /** The SS4.13 set, minus the three mutators `Session` drives itself. */ get pending(): PendingCommandView; /** * Fold one view event, then fan it out to the turn handles that want it. * * Throws on an event for another session. Every `ViewEvent` params type * carries a required `sessionId`, and with no `MuseClient` router yet an * embedder feeds this straight off a connection — so on a multiplexed feed * one misrouted frame would silently corrupt this transcript. The * constructor takes a `sessionId`; this is what makes taking it mean * something. */ apply(event: ViewEvent): SessionApplyOutcome; apply(event: { readonly method: string; readonly params?: unknown; }): SessionApplyOutcome; /** Observe SS4.8 fills that did not complete. See `MuseGapFillError`. */ onGapError(handler: GapFillFailureHandler): void; /** * The handle for a turn, created on first mention from either side. A turn * the fold has already settled returns its settled handle, so a wait * registered late resolves instead of hanging on an event that has passed. */ turn(turnId: string): Turn; /** * A host process exited. Classify it, and discharge SS2.13.3b if the profile * demands it (tasks.md T030b, FM-002). * * REPORT EVERY NOTIFICATION OF THE DEATH — the exit promise AND transport * EOF. A repeat report of an already-latched death is not a no-op: it marks * END OF DRAIN and settles waits the drain minted after the first report. * A consumer that dedupes exit notifications and reports the death once * leaves those waits pending forever (FM-001). * * Three outcomes, and the middle one is the one an implementation drops: * - an ORDERLY exit (SS2.11 row 0) is not a death: nothing is discharged * and the session stays usable; * - a DURABLE session's abnormal death reconciles on resume (FM-001), so * the stores are left exactly as observed and only the waiters are told; * - an EPHEMERAL — or unrecognized (SS2.13.1) — session's abnormal death * discharges the obligation in full. */ hostExited(exit: HostDeathNotification): HostDeathDischarge; /** * Submit a user turn (tdd SS3.2) and hand back THIS session's handle for the * turn the ack named. * * The returned handle is the one `apply` already routes events to — not a * second one minted beside it, which would compare equal on every field and * then never receive an item, a delta, or a terminal. * * The wire shaping, the optimistic SS4.13 entry, and the settle-on-rejection * rule all live in `TurnSubmitter`; what is decided HERE is the one thing that * needs the fold: the entry's insertion point, which is the last item this * session holds at submission time. */ sendUserTurn(options: SendUserTurnOptions): Promise; /** Answer approvals with `handler` (FR-019). See `ApprovalRouter`. */ onApproval(handler: ApprovalHandler): void; /** Observe round trips that did not complete. See `ApprovalFailure`. */ onApprovalError(handler: ApprovalFailureHandler): void; /** * Answer a question this session lists in `fold.pendingUserInputs()` (tdd * SS5.10.2): sends `userInput/answer` with one answer per question and * resolves with the host's admission. The question leaves the pending list * only when the server's `userInput/settled` arrives. */ answerUserInput(userInputId: string, answers: readonly UserInputAnswer[]): Promise; /** * Stop the SS3.1.1 retry loop for one entry and take its input back * (SS4.13 "Abandoned"). * * A `Session` method rather than a `pending` view member: the retirement * must also prune the submitter's replay memory, or the abandoned command's * `turn/start` params live for the session's lifetime — unbounded over the * sanctioned submit-then-abandon flow. */ stopRetrying(commandId: string): PendingRetirement | undefined; /** * Feed a consumer-driven replay's answer through the live SS4.13 settlement * rules. Wrapped for the same replay-memory bookkeeping as `stopRetrying`; * `"held"` means the answer settled nothing and the entry stays. */ replayAnswered(commandId: string, answer: ReplayAnswer): PendingRetirement | "held"; /** * Resolve a snapshot join by performing the I/O its plan demands (SS4.13, * SS4.9): same-`commandId` resubmits FIRST, then the replays, then the * reclaimed-signature verdict. * * The order is normative, not incidental: SS4.13 requires an unacked entry be * resubmitted "before any retire-to-composer", because a join miss does not * prove the intake was never written. * * `PendingCommandSet.joinSnapshot` stays available on `pending` for a * fold-only consumer that performs its own I/O; this verb is for the wired * case and therefore requires a connection even when the plan turns out empty. */ resolveSnapshotJoin(facts: SnapshotJoinFacts): Promise[]>; /** * Resolve a reconnect with NO snapshot (`history.mode: "none"`, the default * cursor-resume path). Reconnect itself is the trigger: replay each acked * entry once, resubmit each unacked entry's SAME `commandId` once. */ resolveReconnect(): Promise[]>; }