import { AssistantModeCapabilities } from './modeCapabilities'; import { AssistantModeConfig } from './conversation'; import { AssistantMode } from './types'; /** The role check's latest answer: is it still running, and did it say yes? */ export interface AdminModeAccess { readonly loading: boolean; readonly canUseAdminModes: boolean; } /** * How long a send waits for the role check before giving up on it. * * Long enough that an ordinary round trip always wins the race, short enough * that a check which never answers (an offline tab, a gateway black hole) ends * as a message the person can act on instead of a spinner with no end. Reaching * it is the ONLY way `adminOnlyModeSendRefusal` still sees `loading: true`. */ export declare const ADMIN_MODE_ACCESS_WAIT_MS = 10000; /** * The role check's answer, as something an already-running send can await. * * ★★ Why this exists at all. The mode list hides an admin-only mode from a * non-admin, and an effect resets a persisted one back to Ask — both only AFTER * the check resolves, so an admin's own persisted choice survives the window. * The send had no way to do the same: a `useCallback` closes over one render's * `loading`, so it could either dispatch while the answer was unknown, or refuse. * * Refusing is what shipped first, and it broke every product whose help button * opens the panel and sends on the next tick: an admin whose persisted mode is * an admin-only one got an error turn on every such send, because the check was * still in flight at that instant. Holding is the honest behaviour — the person * asked a question that they are, in fact, allowed to ask. * * A gate is read LIVE through a ref, so it does not keep a "yes" that a later * render has already replaced, the way a value captured by `useCallback` would. * * ★ It is NOT a guarantee about org switches. The gate only ever knows what the * check last told it, and the check learns of a new org one render late — so * for that one commit the gate still holds the previous org's answer. Narrowing * that belongs in the hook (reporting `loading` while the resolved scope is * behind the requested one), not here; the mode list has the same window today. */ export interface AdminModeAccessGate { /** The latest answer, for the final synchronous validation after a wait. */ read(): AdminModeAccess; /** Record the check's current state, releasing every held send once it lands. */ update(access: AdminModeAccess): void; /** * The state to decide on: now if the check has answered, otherwise once it * does — or, at `timeoutMs`, whatever it still says (i.e. still loading). */ wait(timeoutMs?: number): Promise; /** * Settle every held send with what is known right now, and stop the clock. * * ★★ For unmount. Both host apps mount the panel conditionally, so closing it * mid-check destroys the component while a send is still held. Without this * the only thing left to settle that send is its own timeout, which fires up * to `ADMIN_MODE_ACCESS_WAIT_MS` later and writes a refusal into the PERSISTED * conversation — the user reopens the panel and finds a failed turn that was * never actually refused, seconds after they closed it. * * It settles rather than dropping: an unsettled send never runs `handleSend`'s * `finally`, which would leave the conversation marked in-flight for good and * the re-entry guard would then refuse every later send in it. */ abandon(): void; /** Sends currently held. Exposed so a test can prove holding really happens. */ readonly pendingCount: number; } export declare function createAdminModeAccessGate(initial: AdminModeAccess): AdminModeAccessGate; /** * Why this send must not go out, or null when it may — waiting for the role * check first if the mode needs one. * * ★★ A mode with no `requiresAdmin` is never held and never awaits the gate. * That matters for more than tidiness: the default read-only mode is what the * overwhelming majority of sends use, and making those wait on an unrelated * check would add the check's whole latency to the common path. * * The decision itself stays in `adminOnlyModeSendRefusal`, so the send, the mode * list and the reset effect cannot drift apart about which modes are gated. */ export declare function adminOnlyModeSendRefusalAfterAccess(config: { readonly requiresAdmin?: boolean; } | null | undefined, gate: AdminModeAccessGate, timeoutMs?: number): Promise; /** * Why a send in a mode the product does not offer must not go out, or null — * decided when the role check has answered, against the product's mode list AS * IT IS THEN (BOFF-7308). * * An unlisted mode gets the same role-check window as the reset effect, and * `readConfig` is read again afterwards. This honours a list that arrives * DURING that check, but is not a mode-list readiness signal: the product * contract cannot distinguish a loading list from a settled empty one. * * A mode that IS listed is never held: the common path pays nothing. */ export declare function unofferedModeSendRefusalAfterAccess(mode: AssistantMode, readConfig: () => { readonly id: AssistantMode; } | null | undefined, gate: AdminModeAccessGate, timeoutMs?: number): Promise; /** One validated snapshot for the turn's capabilities, consent and voice gates. */ export interface AssistantSendMode { readonly config: AssistantModeConfig | undefined; readonly capabilities: AssistantModeCapabilities; } /** * The one snapshot a send is judged on, taken after BOTH possible access waits * (BOFF-7308). * * ★★ Everything downstream reads THIS, not a value captured when the send * began. A held send used to re-read `requiresAdmin` from the live list while * keeping the capabilities resolved before the wait, so a turn in a mode whose * config landed during the hold ran on built-in defaults: the wrong stream * budget, and artifacts never collected for a mode configured to produce them. * A removed mode must not fall back to its old config either. * * ★ Roles are the only thing waited for. A mode list that loads independently * of them needs a readiness signal the product seam does not carry today — * `AssistantProductProvider` has no "modes still loading" flag, and a settled * empty list is indistinguishable from a loading one. Adding that contract, * shared with the reset effect, is a separate change. */ export declare function resolveAssistantSendModeAfterAccess(mode: AssistantMode, readConfig: () => AssistantModeConfig | undefined, gate: AdminModeAccessGate, timeoutMs?: number): Promise; //# sourceMappingURL=adminModeSendGate.d.ts.map