/** * manager.ts, the hosted-session engine: lifecycle, policy, durability. * * ── What a hosted session is ─────────────────────────────────────────────── * * The same conversation loop a terminal runs, composed inside the daemon: the * real Orchestrator, the real tool registry rooted at a workspace, the product's * own permission manager with its trust gate. session-runtime.ts builds one; * workspace-floor.ts shares what a workspace's sessions have in common; this * file owns their lives. * * ── Detach is a policy, and its default preserves what people expect ─────── * * When the last attached client goes away, the effective policy decides: `kill` * (the default, and what every surface has always done) terminates the session * with the reason `detached`; `survive` leaves it idle and reattachable. The * default is the SETTING `hostedSessions.detachPolicy`, and a session may carry * its own override chosen at creation. The capability lands; the familiar * behavior stays the default. * * ── Streaming ────────────────────────────────────────────────────────────── * * Token deltas, tool calls and turn transitions do NOT need a new channel. The * Orchestrator already emits them on the runtime bus, stamped with this * session's id, and the control-plane SSE stream already forwards every runtime * domain a client subscribed to. A client attached to a hosted session watches * `turn` and `tools` exactly as it would locally, and filters on the session id * it was handed. * * What was genuinely missing is LIFECYCLE: which hosted sessions exist, when * one was created, attached, detached or terminated, and why. That is this * engine's own channel (`hosted-session-update`, domain `session`). * * ── Restart ──────────────────────────────────────────────────────────────── * * A daemon restart is reconciled honestly, never silently. Every restored * session is either resumable, restored idle, with a system line in its * transcript saying its turn was interrupted, or it is terminated with a named * reason. Nothing comes back pretending it never stopped, and nothing * disappears without a record. */ import type { RuntimeEventBus } from '../runtime/events/index.js'; import type { SessionLiveTurnControls } from '../control-plane/routes/session-runtime.js'; import { type HostedWorkspaceFloorFactory } from './workspace-floor.js'; import { HostedSessionStore, type HostedSessionLoadReport } from './store.js'; import { type HostedSessionSpine } from './spine-intake.js'; export type { HostedSessionSpine } from './spine-intake.js'; import type { CreateHostedSessionInput, HostedDetachPolicy, HostedSessionHistoryMessage, HostedSessionRecord, HostedSessionTerminationReason } from './types.js'; /** The wire event every hosted-session lifecycle notice is published on. */ export declare const HOSTED_SESSION_WIRE_EVENT = "hosted-session-update"; /** The subset of the control-plane gateway this engine publishes through. */ export interface HostedSessionEventPublisher { publishEvent(event: string, payload: unknown, filter?: { clientId?: string; }): void; /** * Live control-plane clients, when the publisher is the gateway. Read as the * second renewal signal for an attachment lease, see ./attachments.ts. */ listClients?(): readonly { readonly id: string; }[]; } /** The live settings this engine reads. Read on every use, never cached. */ export interface HostedSessionSettings { /** `hostedSessions.detachPolicy`, the default when a session carries no override. */ detachPolicy(): HostedDetachPolicy; /** `hostedSessions.maxSessions`, the cap on LIVE (non-terminated) sessions. */ maxSessions(): number; /** `hostedSessions.attachmentTtlMs`, how long an attachment stands unrenewed. */ attachmentTtlMs?(): number; } /** Per-session live-turn registration, so the session verbs can reach a hosted turn. */ export interface HostedLiveTurnRegistry { bindSession(sessionId: string, controls: SessionLiveTurnControls): void; unbindSession(sessionId: string, controls: SessionLiveTurnControls): void; } export interface HostedSessionManagerOptions { readonly floorFactory: HostedWorkspaceFloorFactory; readonly store: HostedSessionStore; readonly settings: HostedSessionSettings; /** The runtime bus turn events are observed on, the daemon's own. */ readonly runtimeBus: RuntimeEventBus; /** The base system prompt for a hosted turn, per session. */ readonly systemPrompt: (input: { readonly sessionId: string; readonly workspaceRoot: string; }) => string; /** Registers each hosted session's live-turn controls; omitted ⇒ the session verbs cannot reach hosted turns. */ readonly liveTurns?: HostedLiveTurnRegistry | undefined; /** The shared session broker, so hosted sessions appear in `sessions.list`. */ readonly spine?: HostedSessionSpine | undefined; /** Whether a workspace root is acceptable. Omitted ⇒ any absolute path. */ readonly isWorkspaceUsable?: ((workspaceRoot: string) => boolean) | undefined; /** * How often queued inputs are collected and each live session's participant * heartbeat is refreshed. Default 750ms, the same order as every other * inbound-dispatch client here, and the reason a steer reaches a hosted turn * in well under a second rather than on some slower sweep. */ readonly intakeIntervalMs?: number | undefined; /** Clock seam for tests. */ readonly now?: (() => number) | undefined; /** How often lapsed attachments are swept. Omitted ⇒ see attachmentSweepIntervalFor. */ readonly attachmentSweepIntervalMs?: number | undefined; } /** What `attach` hands back: the record plus the history a client renders. */ export interface HostedSessionAttachment { readonly session: HostedSessionRecord; readonly history: readonly HostedSessionHistoryMessage[]; } export declare class HostedSessionManager { private readonly options; private readonly sessions; private readonly floors; private readonly now; private publisher; private alerter; private busUnsubscribers; private disposed; private lastLoadReport; /** The spine half: registration, heartbeats, and collecting queued inputs. */ private readonly spine; /** Sweeps attachments whose lease ran out, see ./attachments.ts. */ private attachmentTimer; constructor(options: HostedSessionManagerOptions); /** Where an incident nobody is attached to see goes. Wired by the daemon. */ setOwnerAlerter(alerter: ((text: string) => void) | null): void; /** The configured attachment lease, clamped. Read live, never cached. */ private attachmentLeaseMs; /** Where lifecycle notices go. Wired by the composition that owns the gateway. */ setEventPublisher(publisher: HostedSessionEventPublisher | null): void; /** * Restore from disk and reconcile. Returns the load report so the caller can * state what happened rather than leaving it in a log line. */ init(): Promise; /** The last load report, for status surfaces. Null before `init`. */ loadReport(): HostedSessionLoadReport | null; /** * Decide what a record restored from disk becomes. * * A session that was alive when the process stopped is resumable only when * its effective policy says it should have survived. Anything else is * terminated with the reason that actually applies. */ private reconcileRestored; /** The policy that applies to a session right now. */ private effectivePolicy; /** Every hosted session, newest first. Terminated ones only when asked for. */ list(options?: { readonly includeTerminated?: boolean | undefined; }): readonly HostedSessionRecord[]; /** One record, or null. */ get(sessionId: string): HostedSessionRecord | null; /** Whether this engine hosts a live (non-terminated) session with this id. */ hosts(sessionId: string): boolean; /** The live-turn controls for a hosted session, when its loop is composed. */ liveTurnControls(sessionId: string): SessionLiveTurnControls | null; private refreshEffective; /** Create a hosted session and compose its loop. */ create(input: CreateHostedSessionInput): Promise; /** * Attach a client. Composes the loop when the session came back from disk, * and hands back the history so the client can render what it missed. */ attach(sessionId: string, clientId: string, options?: { readonly leaseMs?: number | undefined; }): Promise; /** * Detach a client and apply the policy when it was the last one. * * Returns the record as it stands afterwards, terminated when the policy * said kill, idle and reattachable when it said survive. */ detach(sessionId: string, clientId: string): Promise; /** Begin expiring attachments nobody renewed. Idempotent. */ private startAttachmentSweep; /** * One sweep. A lapsed attachment goes through `detach` rather than being * removed quietly: the client did leave, it just never said so, and the * session's own policy decides whether that ends it. Public so a test can * drive it without waiting on a timer. */ reapAttachments(): Promise; /** End a hosted session on request. */ kill(sessionId: string, reason?: HostedSessionTerminationReason): Promise; /** * Drive a turn on a hosted session: the path `sessions.steer` / * `sessions.followUp` reach, and the one `create`'s initial prompt uses. */ deliver(sessionId: string, text: string): Promise; /** The conversation as a client renders it. Empty for a session with no loop yet. */ private history; /** Read a hosted session's history without attaching. */ historyOf(sessionId: string): readonly HostedSessionHistoryMessage[]; /** Compose a restored session's loop on first use, replaying its transcript. */ private ensureComposed; private replayConversation; /** * Take a session's loop apart, keeping its transcript. * * The capture is the load-bearing half: `persist` reads the conversation off * the live runtime, so disposing first and persisting after would write an * empty transcript over a real one, a session that came back from a restart * with nothing in it, which is the silent loss this engine exists to avoid. */ private teardownRuntime; private terminate; /** * Turn transitions, observed on the shared bus and attributed by session id. * * The engine does not wrap the orchestrator to learn this: the events it * already emits are the truth, and reading them keeps one source rather than * two that can disagree. */ private observeTurnEvents; private publish; private persist; private requireWorkspace; private requireKnown; private requireLive; private assertUsable; /** * Stop hosting. Every live session is terminated with `daemon-shutdown` and * persisted, so the next start reconciles from a record that says what * happened rather than from one that claims it is still running. */ dispose(): Promise; /** * Park a surviving session across a shutdown: loop down, record kept idle, * transcript written. */ private parkForShutdown; } /** A hosted session id nobody here knows. */ export declare class HostedSessionNotFoundError extends Error { readonly sessionId: string; constructor(sessionId: string); } /** A known hosted session that cannot serve this request, with the reason. */ export declare class HostedSessionUnavailableError extends Error { readonly sessionId: string; constructor(sessionId: string, reason: string); } /** A malformed request argument. */ export declare class HostedSessionArgumentError extends Error { constructor(message: string); } /** The configured hosted-session cap is reached. */ export declare class HostedSessionLimitError extends Error { constructor(message: string); } //# sourceMappingURL=manager.d.ts.map