import { PersistentStore } from '../state/persistent-store.js'; import type { RuntimeEventBus } from '../runtime/events/index.js'; import { RouteBindingManager } from '../channels/index.js'; import type { ConversationGateConfigReader } from '../agents/conversation-gate.js'; import type { SharedSessionCompletion, SharedSessionContinuationRunner, SharedSessionInputRecord, SharedSessionSurfaceReplyBinder } from './session-intents.js'; import type { CreateSharedSessionInput, EnsureSharedSessionInput, FindSharedSessionOptions, ListSharedSessionsOptions, RegisterSharedSessionInput, SharedSessionMessage, SharedSessionRecord, SharedSessionRegisterResult, SharedSessionSubmission, SteerSharedSessionMessageInput, SubmitSharedSessionMessageInput } from './session-types.js'; import { type SharedSessionAgentStatusProvider, type SharedSessionEventPublisher, type SharedSessionMessageSender, type SharedSessionStoreSnapshot } from './session-broker-helpers.js'; export declare class SharedSessionBroker { private readonly store; /** The file this broker serves from, or null for an injected store. Boot folds/sweeps must NAME it, never re-derive it, see daemon/daemon-session-store-boot.ts. */ readonly storePath: string | null; private readonly routeBindings; private readonly agentStatusProvider; private readonly messageSender; private readonly conversationGateConfig; private readonly sessions; private readonly messages; private readonly inputs; private readonly runtimeBusBridge; private readonly writes; private eventPublisher; private continuationRunner; private surfaceReplyBinder; private surfaceNoticeSender; private loaded; private _gcInterval; private externalLivenessProbe; /** Default idle threshold for zero-message sessions (ms). */ private readonly _idleEmptyMs; /** Default idle threshold for sessions with content (ms). */ private readonly _idleLongMs; /** Retention window (ms since closedAt) for CLOSED sessions; Infinity = retain forever. */ private readonly _deletionRetentionMs; /** @param config idleEmptyMs (empty-session idle, default 10m), idleLongMs (default * 24h), deletionRetentionMs (closed-session delete age, default Infinity = retain). */ constructor(config: { readonly store?: PersistentStore | undefined; readonly storePath?: string | undefined; readonly routeBindings: RouteBindingManager; readonly agentStatusProvider: SharedSessionAgentStatusProvider; readonly messageSender: SharedSessionMessageSender; readonly idleEmptyMs?: number | undefined; readonly idleLongMs?: number | undefined; readonly deletionRetentionMs?: number | undefined; /** Reads `conversationGate.*` so an inbound channel message is not handed to a running agent behind the gate's back; absent falls back to the gate's defaults, which gate every channel surface. */ readonly conversationGateConfig?: ConversationGateConfigReader | undefined; }); /** Liveness for turns running outside this broker (see session-broker-gc.ts's isExternallyLive). Set post-construction: the answering subsystem takes this broker as its spine. */ setExternalLivenessProbe(probe: ((session: SharedSessionRecord) => boolean) | null): void; setEventPublisher(publisher: SharedSessionEventPublisher | null): void; /** * Returns the number of sessions that currently have a pending input * (i.e. pendingInputCount > 0). Used by WorkspaceSwapManager to determine * whether the daemon is busy before allowing a workspace swap. */ countBusySessions(): number; /** * Gracefully stop the broker by clearing the GC interval, tearing down bus * subscriptions, and persisting state. Call from DaemonServer.stop(). */ stop(): Promise; /** * Wire the broker to a RuntimeEventBus so agent terminal events automatically * reconcile session inputs and task state. * * Call once after both the broker and the bus are constructed. Returns an * unsubscribe function that tears down the subscriptions. * * @param bus - The active RuntimeEventBus. * @param sessionResolver - Maps agentId → sessionId for the active session. * Return `null` when the agent is not associated with a shared session. */ attachRuntimeBus(bus: RuntimeEventBus, sessionResolver: (agentId: string) => string | null): () => void; setContinuationRunner(runner: SharedSessionContinuationRunner | null): void; /** * Install the hook that routes an agent's answer back to the channel the * message arrived on. See SharedSessionSurfaceReplyBinder, the broker * announces every (agent, surface-originated input) pairing through it, so a * host wires the reply path once instead of per adapter. */ setSurfaceReplyBinder(binder: SharedSessionSurfaceReplyBinder | null): void; /** * Install the path for a one-line unsolicited message to a route's channel, * distinct from the reply binder above, which pairs an AGENT's answer with a * conversation. Today's only caller is the route-binding healing in * session-broker-intent.ts, telling a chat its conversation moved. */ setSurfaceNoticeSender(sender: ((routeId: string, text: string) => void) | null): void; private announceSurfaceReply; start(): Promise; listSessions(limit?: number, options?: ListSharedSessionsOptions): SharedSessionRecord[]; getSession(sessionId: string): SharedSessionRecord | null; findPreferredSession(options?: FindSharedSessionOptions): Promise; ensureSession(input?: EnsureSharedSessionInput): Promise; /** Idempotent register/heartbeat; a brand-new session is born ALREADY * surface-managed (avoids a create-then-patch race where a concurrent steer * sees active-but-not-yet-managed and hits the executor path). */ register(input: RegisterSharedSessionInput): Promise; private markSurfaceManaged; getMessages(sessionId: string, limit?: number): SharedSessionMessage[]; getInputs(sessionId: string, limit?: number): SharedSessionInputRecord[]; createSession(input?: CreateSharedSessionInput): Promise; closeSession(sessionId: string): Promise; reopenSession(sessionId: string): Promise; /** * Permanently remove a shared session record and its queued messages/inputs * from the home-scoped store (see CHANGELOG 1.0.0: a real hard-delete verb, distinct from * `closeSession`, closed sessions are HISTORY and are never touched by this * path unless explicitly asked). Requires the session to already be closed: * deleting a still-active session returns `'active'` so the caller can * surface an honest 409 (close it, then delete) rather than yanking a * record out from under a live participant/agent. An unknown OR * already-deleted id returns `'not-found'`, delete is not a 200-noop; a * second delete of the same id is an honest 404 at the route layer. * * Emits `session-deleted` on the same `session-update` wire channel as * close/reopen/detach so subscribers drop the row live. */ deleteSession(sessionId: string): Promise<'deleted' | 'not-found' | 'active'>; /** * Detach a surface's participant + route binding without closing or killing the * session ("detach != close != kill"). Emits `session-detached`. Idempotent: * unknown session -> null (404); closed session or no matching participant -> * returned unchanged (a closed session emits no updates, so "stop receiving * updates" is already satisfied). See the module helper `detachSharedSessionParticipant`. */ detachParticipant(sessionId: string, surfaceId: string): Promise; bindAgent(sessionId: string, agentId: string): Promise; submitMessage(input: SubmitSharedSessionMessageInput): Promise; steerMessage(input: SteerSharedSessionMessageInput): Promise; followUpMessage(input: SubmitSharedSessionMessageInput): Promise; appendSystemMessage(sessionId: string, body: string, metadata?: Record): Promise; /** * Persist a companion follow-up message to the shared session message log * without spawning an agent. Called by the companion main-chat send path * (kind='message') so that GET /api/sessions/:id/messages surfaces the message * and TUI subscribers can render it. */ appendCompanionMessage(sessionId: string, input: { readonly messageId: string; readonly body: string; readonly timestamp: number; readonly source: string; readonly metadata?: Readonly> | undefined; }): Promise; completeAgent(sessionId: string, agentId: string, body: string, metadata?: Record): Promise; cancelInput(sessionId: string, inputId: string): Promise; rebindRoute(bindingId: string, sessionId: string): Promise; private appendMessage; private attachParticipantAndRoute; private resolveActiveAgentId; private resolveBinding; private buildContinuationTask; /** Snapshot now, write in call order, `gcSweep` persists unawaited and would * otherwise land a stale view over a `cancelInput`. See StoreWriteQueue. */ private persist; private publishUpdate; private publishInputLifecycleEvent; private handleIntent; /** Collection read for a live surface (see the module helper `filterSessionInputsSince`). */ getInputsSince(sessionId: string, options?: { readonly state?: SharedSessionInputRecord['state'] | undefined; readonly since?: number | undefined; readonly limit?: number | undefined; }): SharedSessionInputRecord[]; /** A live surface reports a collected input delivered (`consumed:false`) or * consumed/completed (`consumed:true`), optionally naming the agent answering it. * Lifecycle, the agent pairing and the events live in `applySurfaceInputDelivery`. */ markInputDelivered(sessionId: string, inputId: string, options?: { readonly consumed?: boolean | undefined; readonly agentId?: string | undefined; }): Promise; /** * A surface reports a collected input it could not act on. * * The counterpart to `markInputDelivered(consumed: true)`, and the reason * that call is not the only terminal one: a surface that collected an input * and then failed to hand it to its loop used to mark it completed anyway, * which is a record saying the owner's message was answered when nothing * received it. This moves it to `failed` with the reason attached, so the * lifecycle event says what happened and the record can be read afterwards. */ failInput(sessionId: string, inputId: string, error: string): Promise; private runQueuedFollowUp; private sessionInputStore; private messageStore; private touch; private refreshPendingInputCount; /** Periodic sweep: idle-close active sessions and (only under a finite retention * window) delete closed ones. Full policy lives in the module helper `sweepSharedSessions`. */ /** Retained in-memory record count (sessions + message/input bucket entries), for MemoryGovernor visibility. */ retainedRecordCount(): number; /** * MemoryGovernor trim hook, a REAL reclaim. `floor` runs the idle/closed * session GC sweep immediately; `flush` additionally truncates the message * and input buckets of every non-busy session to a short tail (the full * transcript persists in the session store; these buckets are the live * relay mirror). */ trimRetained(level: 'floor' | 'flush'): void; private gcSweep; } //# sourceMappingURL=session-broker.d.ts.map