/** * workspace-floor.ts, one client-shape composition per WORKSPACE, shared by * every hosted session in it. * * ── The choice this file records ─────────────────────────────────────────── * * The obvious shape is one `createClientRuntimeServices` per hosted session. * It is also the wrong one, and the floor's own construction says why. Building * a floor costs, per composition: * * - a provider stack, including a model-DISCOVERY pass at construction * (provider-stack.ts: `initProviderModelDiscovery`), a network round trip * per configured provider; * - filesystem watchers on the config tree (`watchConfigFiles`) plus a * config→bus bridge; * - a plugin manager and an MCP registry, each of which reaches processes and * files that exist once per machine; * - a `ProjectIndex` and a `FileStateCache` over the workspace tree. * * Every one of those is a per-machine or per-workspace truth. Duplicating them * per session buys no isolation, two sessions in one workspace would discover * the same models, watch the same files, and index the same tree, and costs * watcher handles and discovery traffic linear in session count. The file cache * and project index are the sharpest case: they exist precisely SO tools share * cache state, and two sessions editing one workspace with two caches would * disagree about the file they both just wrote. * * What genuinely differs per session is the conversation and the turn: message * history, the queued mid-turn messages, the in-flight tool-call aborts, the * live-turn controls, context accounting. Those all belong to the Orchestrator * and its own tool registry, which is exactly what hosted-session-runtime.ts * builds per session, over the floor this file shares. * * So: floors are keyed by resolved workspace root, reference-counted by the * sessions using them, and disposed when the last one goes. A workspace with no * live hosted session holds no watchers. * * ── Why the factory is injected ──────────────────────────────────────────── * * The floor's `requestApproval` seam is where a product's trust posture lives, * the daemon puts its workspace trust gate there, and that gate reads a * decision file under the workspace being asked about. A floor built for * workspace W must therefore be built with W's gate, which only the product can * supply. The engine owns the CACHING and the lifetime; the product owns what a * floor is made of. * * The exec POSTURE ({@link HostedWorkspaceFloor.execPosture}) is here for the * same reason and no other: it is a statement about how much authority a run on * this floor carries, which is the product's to make. The engine's own default *, stated when a floor says nothing, is the contained one. */ import type { ClientRuntimeServices } from '../runtime/client-services.js'; import type { WrfcController } from '../agents/wrfc-controller.js'; import type { HostedExecPostureDecider } from './exec-posture.js'; /** * A composed floor for one workspace, plus whatever the product wired around * it that a turn also needs. */ export interface HostedWorkspaceFloor { /** The client-shape composition every session in this workspace runs on. */ readonly services: ClientRuntimeServices; /** * Review-chain listing for the orchestrator's services bag. Omitted ⇒ this * floor runs no review/fix chains, and the orchestrator is told so by being * handed a listing that reports none, an honest empty answer rather than a * missing dependency. */ readonly wrfcController?: Pick | undefined; /** * What a session on this floor may do with exec, decided per session. * * Omitted ⇒ `conversational`: the exec boundary is REQUIRED, so a command * that cannot be contained refuses rather than running on the host, and the * owner's terminal is denied. That is the posture every session created over * `sessions.hosted.*` runs under. * * A product returns `workstream` only for a spawn it composed itself, for a * work chain the owner authorized. It is a function on the FLOOR and not a * field on `CreateHostedSessionInput` deliberately: a caller over the wire * must have no spelling that reaches the host. */ readonly execPosture?: HostedExecPostureDecider | undefined; /** Release everything this floor started. Called when its last session goes. */ dispose(): void | Promise; } /** How a product builds a floor for one workspace. */ export type HostedWorkspaceFloorFactory = (input: { readonly workspaceRoot: string; }) => HostedWorkspaceFloor | Promise; /** A borrowed floor. `release()` is idempotent and drops one reference. */ export interface HostedWorkspaceFloorLease { readonly floor: HostedWorkspaceFloor; release(): void; } /** * The floor cache. One instance per engine. * * `acquire` is async and single-flighted per workspace: two sessions created in * the same workspace at the same moment share one construction rather than * racing two provider-discovery passes against each other. */ export declare class HostedWorkspaceFloors { private readonly factory; private readonly entries; private readonly pending; private disposed; constructor(factory: HostedWorkspaceFloorFactory); /** How many workspaces currently hold a composed floor. */ size(): number; /** The workspace roots with a live floor, for status reporting. */ workspaces(): readonly string[]; acquire(workspaceRoot: string): Promise; private resolveEntry; private retire; /** Dispose every floor. Idempotent. */ dispose(): Promise; } //# sourceMappingURL=workspace-floor.d.ts.map