// Sessions remain resident after creation so their open Remote sources keep running off-screen.
import { randomUUID } from '@deepseek-ai/dsh-util-crypto';
import { SessionEventStream, sessionStreamFailure, } from "../transport.js";
import { transportResult } from "../contract/result.js";
import { MutableSessionEventSource } from "../contract/events.js";
import { Notifier } from "./notifier.js";
import { ProjectionValueStore } from "./projection-store.js";
import { resolvedClientTimeZone } from "../time-zone.js";
import { SessionQueueMirror } from "./queue-mirror.js";
/** Messages requested per history page. */
export const PAGE_MESSAGES = 50;
/**
 * Owns a session's event window, lifecycle state, and observable
 * snapshot. React bindings remain outside this data layer. Features see only
 * the {@link SessionFace} slice (ISession verbs + the snapshot source); the
 * remaining public members are Session Controller internals.
 */
export class Session {
    sessionId;
    remote;
    options;
    // ---- Window and derived state (all private; the snapshot is the only read API) ----
    baseSeq = 0;
    hasMore = false;
    openState = 'cold';
    openError = null;
    openPromise = null;
    /** Bumped by stream replacement to invalidate an in-flight doOpen. Stale
     *  passes drop all writes once the generation moves on. */
    openGeneration = 0;
    loadingOlder = false;
    /** Authoritative stream-only inbox snapshot; pending work never hits history. */
    queueMirror = new SessionQueueMirror();
    running = false;
    address;
    parentAvailable;
    /**
     * Sticky send marker, private input of the composerPhase derivation: set
     * synchronously before prompt()'s first await, never reset — the blank →
     * engaging edge of the phase machine (see ComposerPhase).
     */
    promptAttempted = false;
    /** A first accepted prompt stays in the engaging phase until its turn is observable. */
    firstPromptPendingTurn = false;
    /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
    blankBit = true;
    removed = false;
    promptError = null;
    lastAgentError = null;
    /** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
    pendingSubmissions = [];
    /** Per-echo settlement state; `retiring` latches the first observation so a
     *  queue frame and its durable event cannot both retire one echo. */
    submissionSettlements = new Map();
    /** Owns the addressed page/follow lifecycle while this Session is open. */
    events;
    /**
     * Per-session projection value store (push model; see the session-projection
     * subsystem page, docs/subsystems/session-projection.md): finished whole
     * values computed on the Host, seeded by the tail page's
     * projections block and updated by Session Controller control frames under the
     * one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
     * (the useProjection resolution face); the conversation snapshot never
     * carries projection values, and no client-side domain folding exists.
     * Manager-owned when constructed through SessionManager (frames route and
     * the store outlives instantiation, the title-snapshot precedent); a bare
     * construction gets a private store.
     */
    projections;
    /** Contiguous history and live tail consumed by Conversation assembly. */
    eventSource = new MutableSessionEventSource();
    snapshotCache;
    notifier;
    /**
     * Agent-scoped cordis context, bound once by ClientSessions when it
     * mints the scope (the client mirror of the host Agent's loopCtx). The
     * Session dispatches its own scoped events through it; undefined means
     * unbound (bare object-layer construction) or already pruned — both skip
     * dispatch-dependent behavior rather than fail.
     */
    actx;
    /**
     * @param sessionId - Host session identity (client sessions are always Host-born).
     * @param remote - generated Remote namespaces this session calls.
     * @param options - optional manager-owned state observers.
     */
    constructor(sessionId, remote, options = {}) {
        this.sessionId = sessionId;
        this.remote = remote;
        this.options = options;
        this.projections = options.projections ?? new ProjectionValueStore();
        this.address = options.address;
        this.parentAvailable = options.parentAvailable;
        this.notifier = new Notifier(() => {
            this.snapshotCache = this.buildSnapshot();
        });
        this.snapshotCache = this.buildSnapshot();
    }
    /**
     * Bind the Agent-scoped context minted by ClientSessions (single write;
     * a second bind is a wiring error and throws). Direction stays one-way at
     * this binding boundary: consumers still reach the Session via `sessions.sessionOf`,
     * while the Session holds its own dispatch point (host Agent.loopCtx
     * mirror).
     * @param actx - the agent's scoped context.
     */
    bindScope(actx) {
        if (this.actx !== undefined)
            throw new Error(`session ${this.sessionId} already has a bound scope`);
        this.actx = actx;
    }
    /** Release the bound scope at prune time (a later rebind accompanies a freshly minted scope). */
    unbindScope() {
        this.actx = undefined;
    }
    // ---- Operations ----
    /**
     * Register one local submission echo (see the ISession declaration).
     * Synchronous through markDirty: the echo is in the very next snapshot, so
     * the conversation can paint it before the caller starts serializing.
     * @param input - echo content and the optional settlement callback.
     * @returns the minted identity for {@link prompt} plus the pre-prompt abandon path.
     */
    beginSubmission(input) {
        const requestId = randomUUID();
        this.pendingSubmissions = [...this.pendingSubmissions, {
                requestId,
                time: Date.now(),
                text: input.text,
                images: input.images,
            }];
        this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false });
        // The blank → engaging edge flips here, ahead of prompt(): the composer
        // docks and the echo renders on the click's own frame.
        this.promptAttempted = true;
        this.notifier.markDirty();
        return { requestId, abandon: () => { this.retireFailedSubmission(requestId); } };
    }
    /**
     * Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
     * @param content - text plus browser-owned temporary image uploads.
     * @param mode - queue appends after the current turn; steer interrupts it.
     * @param signal - optional caller cancellation for the complete admission round-trip.
     * @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
     * @returns the prompt result (also mirrored into promptError on failure).
     */
    async prompt(content, mode, signal, requestId) {
        this.promptError = null;
        this.lastAgentError = null;
        // Synchronous, before the first await: the blank → engaging edge must be
        // visible on the session area's very first frame when a caller sends
        // ahead of navigation (first-send flow).
        this.promptAttempted = true;
        if (this.blankBit)
            this.firstPromptPendingTurn = true;
        this.notifier.markDirty();
        let result;
        try {
            if (this.address === undefined) {
                const clientTimeZone = resolvedClientTimeZone();
                result = toSessionResult(await this.remote.session.prompt({
                    requestId: requestId ?? randomUUID(),
                    sessionId: this.sessionId,
                    mode,
                    content,
                    clientTimeZone,
                }, signal));
            }
            else if (this.address.mode === 'one-shot') {
                result = {
                    ok: false,
                    error: {
                        code: 'subagent-not-resumable',
                        message: 'one-shot subagent conversations are read-only',
                        details: { childSessionId: this.address.childSessionId },
                    },
                };
            }
            else {
                if (content.some(part => part.type === 'image')) {
                    result = {
                        ok: false,
                        error: {
                            code: 'attachment-error',
                            message: 'Image input is unavailable for subagent continuations.',
                            details: { reason: 'SUBAGENT_IMAGE_UNSUPPORTED' },
                        },
                    };
                }
                else {
                    const routed = toSessionResult(await this.remote.subagents.prompt({
                        requestId: randomUUID(),
                        parentSessionId: this.address.parentSessionId,
                        childSessionId: this.address.childSessionId,
                        mode: this.address.mode,
                        content: content.flatMap(part => part.type === 'text'
                            ? [{ type: 'text', text: part.text }]
                            : []),
                        clientTimeZone: resolvedClientTimeZone(),
                    }, signal));
                    result = routed.ok ? { ok: true, value: { accepted: true } } : routed;
                }
            }
        }
        catch (error) {
            result = transportResult(error);
        }
        if (!result.ok) {
            if (requestId !== undefined)
                this.retireFailedSubmission(requestId);
            this.promptError = { op: 'send', error: result.error };
            this.notifier.markDirty();
            return result;
        }
        // Blank flips on ACCEPTANCE, not attempt: an accepted prompt starts the
        // conversation's first turn on the host (the host criterion — a logged
        // turn/start — is fact, not optimism; standalone command and projection
        // events never flip it), while a rejected first prompt must keep the
        // session blank — the client-side blank mirror only ever lowers, so
        // flipping early on a failure would surface the session forever and
        // strip its connectWorkspace reuse eligibility against the host's
        // authority.
        if (this.blankBit) {
            this.blankBit = false;
            this.options.onEngaged?.(this);
            this.notifier.markDirty();
        }
        return result;
    }
    /**
     * Resolve one image referenced by this session into browser-consumable bytes.
     * @param attachmentId - opaque id found in the folded session log.
     * @returns the authenticated reference and decoded bytes.
     */
    async readAttachment(attachmentId) {
        try {
            const result = await this.remote.session.attachment({
                sessionId: this.sessionId,
                attachmentId,
            });
            if (!result.ok)
                return toSessionResult(result);
            const binary = atob(result.value.data);
            const data = Uint8Array.from(binary, char => char.charCodeAt(0));
            return { ok: true, value: { attachment: result.value.attachment, data } };
        }
        catch (error) {
            return transportResult(error);
        }
    }
    /** Apply one operation to a still-pending queue occurrence. */
    async updateQueue(itemId, action) {
        try {
            return toSessionResult(await this.remote.session.updateQueue({ sessionId: this.sessionId, itemId, action }));
        }
        catch (error) {
            return transportResult(error);
        }
    }
    /**
     * Stop the active turn while the Host preserves pending inbox work; failures
     * land in promptError (same error-strip display slot). A continuable
     * subagent address routes through `subagents.interruptByParent`, whose durable
     * parent-address authority works without a live parent Agent; a one-shot
     * address stays uncancellable (the UI offers no stop action, so this arm is
     * defensive).
     * @returns the cancel result.
     */
    async cancel() {
        const address = this.address;
        if (address !== undefined && address.mode === 'one-shot') {
            const result = {
                ok: false,
                error: {
                    code: 'subagent-delivery-unavailable',
                    message: 'subagent activation cancellation is unavailable',
                    details: { childSessionId: address.childSessionId },
                },
            };
            this.promptError = { op: 'stop', error: result.error };
            this.notifier.markDirty();
            return result;
        }
        let result;
        try {
            result = address !== undefined
                ? toSessionResult(await this.remote.subagents.interruptByParent(address.childSessionId, address.parentSessionId, address.mode))
                : toSessionResult(await this.remote.session.cancel({ sessionId: this.sessionId }));
        }
        catch (error) {
            result = transportResult(error);
        }
        if (!result.ok) {
            this.promptError = { op: 'stop', error: result.error };
            this.notifier.markDirty();
        }
        return result;
    }
    /**
     * Rename: contract session.rename 1:1. On success settle the 'title'
     * projection cell from the response's `{title, seq}` under the store's
     * higher-seq-wins rule (the push frame arriving later is a no-op replay),
     * so the list row and any useProjection('title') reader update without
     * waiting for the control-stream projection update.
     * @param title - raw title text (the host normalizes acceptance).
     * @returns the rename result (normalized accepted title + title event seq).
     */
    async rename(title) {
        try {
            const result = toSessionResult(await this.remote.session.rename({ sessionId: this.sessionId, title }));
            if (result.ok)
                this.projections.apply('title', result.value.title, result.value.seq);
            return result;
        }
        catch (error) {
            return transportResult(error);
        }
    }
    /**
     * Execute one slash-command line against this session's agent — pure
     * admission semantics (the host executor durably logs the lifecycle;
     * outcomes render as flow nodes, never as a response echo).
     * @param line - the full command line, leading slash included.
     * @returns the admission result, or the error branch on transport failure.
     */
    async command(line) {
        const result = await this.remote.commands.execute(this.sessionId, line, []);
        if (!result.ok)
            return result;
        return { ok: true, value: { matched: result.value !== undefined } };
    }
    /** First open: pull the tail page (idempotent — in-flight/already-open returns the existing promise). */
    open() {
        if (this.openState === 'open')
            return Promise.resolve();
        if (this.openPromise !== null)
            return this.openPromise;
        const promise = this.doOpen(this.openGeneration).finally(() => {
            // Identity-guarded: a superseded open must not null out the promise resync just started.
            if (this.openPromise === promise)
                this.openPromise = null;
        });
        this.openPromise = promise;
        return promise;
    }
    /** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend. */
    async loadOlder() {
        if (this.openState !== 'open' || !this.hasMore || this.loadingOlder)
            return;
        const events = this.events;
        if (events === undefined)
            return;
        this.loadingOlder = true;
        this.notifier.markDirty();
        try {
            await events.prepend({ beforeSeq: this.baseSeq, maxMessages: PAGE_MESSAGES });
        }
        catch (error) {
            if (sessionStreamFailure(error) === undefined) {
                console.error('[session-controller] loadOlder failed:', error);
            }
        }
        finally {
            this.loadingOlder = false;
            this.notifier.markDirty();
        }
    }
    /** Rebuild an opened history source after address replacement.
     *  Invalidates any in-flight open first; queue state belongs to the independently
     *  reconnecting control stream and remains untouched. */
    async resync() {
        if (this.openState === 'cold')
            return; // never opened: no window to rebuild (doOpen flips to 'loading' synchronously, so cold implies no in-flight open)
        this.openGeneration++;
        const events = this.events;
        this.events = undefined;
        await events?.dispose();
        this.openPromise = null;
        this.openState = 'cold';
        this.openError = null;
        this.baseSeq = 0;
        this.notifier.markDirty();
        await this.open();
    }
    // ---- Subscription API (useSyncExternalStore direct wiring) ----
    /**
     * uSES subscription entry.
     * @param listener - change callback.
     * @returns the unsubscribe function.
     */
    subscribe(listener) {
        return this.notifier.subscribe(listener);
    }
    /**
     * Cached Session snapshot (rebuilt lazily when dirty with no listeners).
     * @returns the cached reference (stable until the next flush).
     */
    getSnapshot() {
        this.notifier.ensureFresh();
        return this.snapshotCache;
    }
    // ---- Manager-only entry points (@internal; never called by the UI) ----
    /**
     * Replace every transient control value for this Session from one stream baseline.
     * @param queue - complete pending queue for this Session.
     */
    replaceControl(queue) {
        this.queueMirror.replace(queue);
        this.observeSubmissionQueue(queue);
        this.notifier.markDirty();
    }
    /**
     * Apply one Session-addressed live control update.
     * @param frame - queue replacement addressed to this Session.
     */
    handleControlFrame(frame) {
        this.queueMirror.replace(frame.items);
        this.observeSubmissionQueue(frame.items);
        this.notifier.markDirty();
    }
    /**
     * Running-bit relay from the host stream (list entry and snapshot stay consistent).
     * @param running - the new running state.
     */
    handleRunning(running) {
        // Turn-start conversion: a blank session never runs, so the first
        // running:true proves another side's first message landed.
        if (running && this.blankBit) {
            this.blankBit = false;
            this.notifier.markDirty();
        }
        if (running)
            this.firstPromptPendingTurn = false;
        if (this.running === running)
            return;
        this.running = running;
        this.notifier.markDirty();
    }
    /**
     * Install or clear the catalog-discovered transport address. A changed
     * address rebuilds an already-open window through its new history route.
     * @param address - direct parent/child address, or undefined for ordinary transport.
     * @param parentAvailable - latest exact-parent availability hint, or undefined before a catalog read.
     */
    configureSubagent(address, parentAvailable) {
        const same = this.address?.parentSessionId === address?.parentSessionId
            && this.address?.childSessionId === address?.childSessionId
            && this.address?.mode === address?.mode;
        this.address = address;
        this.parentAvailable = parentAvailable;
        if (!same && this.openState !== 'cold')
            void this.resync();
        else
            this.notifier.markDirty();
    }
    /**
     * Update only the parent availability hint from a catalog refresh.
     * @param available - whether the exact direct parent is live.
     */
    handleSubagentParentAvailable(available) {
        if (this.parentAvailable === available)
            return;
        this.parentAvailable = available;
        this.notifier.markDirty();
    }
    /**
     * Blank-bit relay from the authoritative summary source (`session.list` and
     * `api-session/added`). Monotone: once any signal (local first send,
     * running flip, an earlier summary) cleared it, a stale true never
     * re-blanks.
     * @param blank - the summary's derived empty-log bit.
     */
    handleBlank(blank) {
        if (blank === this.blankBit)
            return;
        if (blank && (this.promptAttempted || this.running))
            return;
        this.blankBit = blank;
        this.notifier.markDirty();
    }
    /** `api-session/removed` relay: flag the snapshot while retaining the resident instance. */
    handleRemoved() {
        this.removed = true;
        this.notifier.markDirty();
    }
    /**
     * `api-session/error` relay: the outlet for live failures with no turn position.
     * @param message - the stringified error.
     */
    handleAgentError(message) {
        this.lastAgentError = message;
        this.notifier.markDirty();
    }
    /**
     * Stop the Session's live Remote source.
     * @returns when the Remote iterator has completed teardown.
     */
    async dispose() {
        // Unsettled echoes retire as failed so their owners can restore or
        // release browser resources; echoes already scheduled as observed keep
        // that settlement.
        for (const requestId of [...this.submissionSettlements.keys()]) {
            this.retireFailedSubmission(requestId);
        }
        this.openGeneration++;
        const events = this.events;
        this.events = undefined;
        await events?.dispose();
    }
    // ---- Private ----
    /** @param generation - openGeneration at launch; stale passes cannot publish after replacement. */
    async doOpen(generation) {
        this.openState = 'loading';
        this.openError = null;
        this.notifier.markDirty();
        const events = new SessionEventStream(this.remote, this.sessionAddress(), {
            publish: (change) => {
                if (generation !== this.openGeneration || this.events !== events)
                    return;
                this.acceptEventChange(change);
            },
            failed: (error) => {
                this.failEventStream(events, generation, error);
            },
        });
        this.events = events;
        try {
            await events.open({ maxMessages: PAGE_MESSAGES });
            if (generation !== this.openGeneration || this.events !== events)
                return;
            this.openState = 'open';
        }
        catch (error) {
            if (generation !== this.openGeneration || this.events !== events)
                return;
            this.events = undefined;
            this.openState = 'error';
            this.openError = openFailure(error);
        }
        finally {
            if (generation === this.openGeneration)
                this.notifier.markDirty();
        }
    }
    /** Apply one contiguous journal update already reconciled by the Remote stream. */
    acceptEventChange(change) {
        switch (change.type) {
            case 'replace':
                this.installWindow(change.entries, change.hasMore, change.page.projections);
                return;
            case 'prepend':
                this.prependWindow(change.entries, change.hasMore);
                return;
            case 'append':
                if (this.appendLive(change.entry))
                    this.notifier.markDirty();
        }
    }
    /** Replace the complete contiguous window and apply page-owned projection metadata. */
    installWindow(entries, hasMore, projections) {
        this.baseSeq = entries[0]?.event.seq ?? 0;
        this.hasMore = hasMore;
        if (entries.some(entry => entry.event.type === 'turn/start'))
            this.firstPromptPendingTurn = false;
        if (projections !== undefined)
            this.projections.seed(projections);
        this.eventSource.replace(entries, hasMore);
        for (const entry of entries)
            this.observeSubmissionEvent(entry.event);
        this.notifier.markDirty();
    }
    /** Prepend one stream-validated history page. */
    prependWindow(entries, hasMore) {
        this.baseSeq = entries[0]?.event.seq ?? this.baseSeq;
        this.hasMore = hasMore;
        this.eventSource.prepend(entries, hasMore);
    }
    /** Append one stream-validated live event. */
    appendLive(entry) {
        const event = entry.event;
        const awaitingFirstTurn = this.firstPromptPendingTurn;
        if (event.type === 'turn/start')
            this.firstPromptPendingTurn = false;
        const queueChanged = this.queueMirror.acceptDurable(event);
        this.eventSource.append(entry);
        // After the feed append: the conversation assembly's animation frame is
        // registered by the feed subscribers above, so the echo-retirement frame
        // scheduled here always runs after the durable node became renderable.
        this.observeSubmissionEvent(event);
        return queueChanged || awaitingFirstTurn !== this.firstPromptPendingTurn;
    }
    /** Retire the matching echo when a durable browser-prompt `user/message` becomes visible. */
    observeSubmissionEvent(event) {
        if (this.submissionSettlements.size === 0 || event.type !== 'user/message')
            return;
        // Structural read: window entries may be compact history records, so the
        // fields are narrowed rather than trusted (same posture as Conversation
        // assembly matchers).
        const data = event.data;
        const source = data?.source;
        if (source?.kind !== 'user' || typeof source.rpcId !== 'string')
            return;
        this.scheduleObservedRetirement(source.rpcId, imageRefsIn(data?.content));
    }
    /** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
    observeSubmissionQueue(items) {
        if (this.submissionSettlements.size === 0)
            return;
        for (const item of items) {
            if (item.rpcId !== undefined) {
                this.scheduleObservedRetirement(item.rpcId, imageRefsIn(item.message.content));
            }
        }
    }
    /**
     * Latch one observed settlement and remove the echo an animation frame
     * later. The delay keeps the echo in the snapshot until the frame in which
     * the durable node (whose assembly frame was registered first) is
     * renderable; the render-time rpcId dedupe hides the one-frame overlap.
     */
    scheduleObservedRetirement(requestId, attachments) {
        const settlement = this.submissionSettlements.get(requestId);
        if (settlement === undefined || settlement.retiring)
            return;
        settlement.retiring = true;
        scheduleFrame(() => { this.finishSubmission(requestId, { reason: 'observed', attachments }); });
    }
    /** Remove one unsettled echo immediately (prompt rejection, abort, or disposal). */
    retireFailedSubmission(requestId) {
        const settlement = this.submissionSettlements.get(requestId);
        if (settlement === undefined || settlement.retiring)
            return;
        settlement.retiring = true;
        this.finishSubmission(requestId, { reason: 'failed' });
    }
    /** Single removal point: drop the echo, publish, then notify the owner. */
    finishSubmission(requestId, retirement) {
        const settlement = this.submissionSettlements.get(requestId);
        /* v8 ignore next -- retiring latches before every schedule, so one settlement never finishes twice. */
        if (settlement === undefined)
            return;
        this.submissionSettlements.delete(requestId);
        this.pendingSubmissions = this.pendingSubmissions.filter(echo => echo.requestId !== requestId);
        this.notifier.markDirty();
        settlement.onRetire?.(retirement);
    }
    /** Publish a terminal background failure only while this stream still owns the Session. */
    failEventStream(events, generation, error) {
        if (generation !== this.openGeneration || this.events !== events)
            return;
        this.openGeneration++;
        this.events = undefined;
        this.openPromise = null;
        this.openState = 'error';
        this.openError = openFailure(error);
        void events.dispose();
        this.notifier.markDirty();
    }
    buildSnapshot() {
        return {
            sessionId: this.sessionId,
            queue: this.queueMirror.snapshot(),
            pendingSubmissions: this.pendingSubmissions,
            running: this.running,
            subagent: this.address === undefined
                ? null
                : {
                    address: this.address,
                    ...(this.parentAvailable === undefined ? {} : { parentAvailable: this.parentAvailable }),
                },
            removed: this.removed,
            openState: this.openState,
            openError: this.openError,
            hasMore: this.hasMore,
            loadingOlder: this.loadingOlder,
            promptError: this.promptError,
            blank: this.blankBit,
            lastAgentError: this.lastAgentError,
            promptAttempted: this.promptAttempted,
            awaitingFirstTurn: this.firstPromptPendingTurn,
        };
    }
    sessionAddress() {
        return this.address === undefined
            ? { kind: 'session', sessionId: this.sessionId }
            : { kind: 'subagent', ...this.address };
    }
}
/** Run one callback on the next animation frame, or a macrotask where no frame clock exists. */
function scheduleFrame(fn) {
    if (typeof requestAnimationFrame === 'function')
        requestAnimationFrame(() => { fn(); });
    else
        setTimeout(fn, 0);
}
/** Image attachment references in one structurally-read content block list, in block order. */
function imageRefsIn(content) {
    if (!Array.isArray(content))
        return [];
    const refs = [];
    for (const block of content) {
        if (typeof block !== 'object' || block === null)
            continue;
        const candidate = block;
        if (candidate.type === 'image' && typeof candidate.attachment === 'object' && candidate.attachment !== null) {
            refs.push(candidate.attachment);
        }
    }
    return refs;
}
/** Convert a terminal Session stream failure to the Client error vocabulary. */
function openFailure(error) {
    const failure = sessionStreamFailure(error);
    if (failure !== undefined)
        return failure;
    const folded = transportResult(error);
    /* v8 ignore next -- transportResult never returns an ok result. */
    if (folded.ok)
        throw new Error('transportResult returned an unexpected success');
    return folded.error;
}
/** Narrow a generated Session Remote failure to its service-owned error vocabulary. */
function toSessionResult(result) {
    return result.ok ? result : { ok: false, error: result.error };
}
//# sourceMappingURL=session.js.map