/** * SessionStateBag — typed container for the per-session state previously held * as private fields on `AgentService` (six `Map` instances with * no shared cleanup path). * * Goals: * - Single `disposeSession(sessionKey)` entry that clears every slot for a key * (and runs the event unsubscriber, which was easy to forget previously). * - Optional TTL sweep for slots whose lifecycle is not bound to an explicit * register/unregister pair, so long-running Gateways do not accumulate stale * per-session entries from disconnected webchat clients or finished turns. * - Hard upper bound for the same TTL'd slots to bound worst-case memory. * * Slots whose lifetime is fully owned by explicit lifecycle calls * (`inboundTurnDepth`, `taskReviewHintBySession`, `embeddedStreamText`, * `sessionEventUnsubscribers`) are still cleared via `disposeSession`, but they * are NOT subject to TTL — sweeping them would race with their owners. */ export type WebchatStreamPublisher = (event: { type: string; [key: string]: unknown; }) => void; export interface TaskReviewStreamHint { skipTaskReview: boolean; } export interface SessionStateBagOptions { /** Idle TTL for TTL-managed slots. Defaults to 1 hour. Set to 0 to disable sweeping. */ ttlMs?: number; /** Sweep cadence. Defaults to 10 minutes. */ sweepIntervalMs?: number; /** Hard upper bound on entries for TTL-managed slots. Defaults to 5000. */ maxEntries?: number; /** Inject a clock for tests. */ now?: () => number; } export declare class SessionStateBag { /** Webchat run publisher (register/unregister + TTL fallback). */ private readonly webchatPublishers; /** Last assistant plain text (TTL + LRU). */ private readonly lastAssistantText; /** Stream text for the in-flight embedded turn (managed by turn). */ private readonly embeddedStreamText; /** Task review stream state (take-and-delete). */ private readonly taskReviewHintBySession; /** Concurrent inbound turn depth (counter). */ private readonly inboundTurnDepth; /** Agent-event subscription tear-downs (run on dispose). */ private readonly sessionEventUnsubscribers; private readonly ttlMs; private readonly maxEntries; private readonly now; private readonly sweepTimer; constructor(opts?: SessionStateBagOptions); registerWebchatPublisher(sessionKey: string, publisher: WebchatStreamPublisher): void; unregisterWebchatPublisher(sessionKey: string): void; getWebchatPublisher(sessionKey: string): WebchatStreamPublisher | undefined; setLastAssistantText(sessionKey: string, text: string): void; getLastAssistantText(sessionKey: string): string | undefined; appendEmbeddedStreamText(sessionKey: string, chunk: string): string; clearEmbeddedStreamText(sessionKey: string): void; recordTaskReviewStreamHint(sessionKey: string, task: TaskReviewStreamHint): void; takeTaskReviewStreamHint(sessionKey: string): TaskReviewStreamHint | undefined; beginInboundTurn(sessionKey: string): void; endInboundTurn(sessionKey: string): void; getInboundTurnDepth(sessionKey: string): number; setSessionEventUnsubscriber(sessionKey: string, unsubscribe: () => void): void; hasSessionEventUnsubscriber(sessionKey: string): boolean; /** Clear every slot for `sessionKey`, invoking the unsubscriber if registered. */ disposeSession(sessionKey: string): void; /** Tear down every session (process stop / hot reload). */ disposeAll(): void; /** Test helper. */ size(): { webchatPublishers: number; lastAssistantText: number; embeddedStreamText: number; taskReviewHintBySession: number; inboundTurnDepth: number; sessionEventUnsubscribers: number; }; private sweepStaleEntries; private enforceCap; }