/** * Subconscious (subc) transport — the daemon-backed alternative to the standalone * NDJSON {@link BinaryBridge}. Implements the SAME {@link AftProjectTransport} / * {@link AftTransportPool} interfaces the plugins consume, so the entire tool / * hoisting / permission / UI surface stays transport-agnostic: only the ONE * construction site (BridgePool vs SubcTransportPool) differs. * * Standalone model: one `aft` child process per project root, session passed * per call. Subc model: ONE {@link SubcClient} per process (one authenticated * daemon connection), and a route opened+cached per `(project_root, harness, * session)` triple — exactly subc's {@link BindIdentity}. So the "pool" here is a * route cache over a single client, not N child processes. * * This module is S2 of B-FINAL: the tool-call route only. The bg_events idle-wake * subscription (S3) and the config gate that selects this transport (S4) build on * top of it. subc-client is a build-time path dependency bundled into the * published plugin dist; it is never a published runtime dependency. */ import type { RouteHandle } from "@cortexkit/subc-client"; import { type BindIdentity, type ConsumerIdentity, type RequestOptions, type RouteTarget, SubcCallError, SubcError } from "@cortexkit/subc-client"; import type { StatusSnapshot } from "./bridge.js"; import { type CanonicalRootPath, type ConcretePoolId, type LifecycleEvent, type LifecyclePoolRegistration, type LifecyclePoolRegistrationOptions, LifecycleRegistry, type RootGeneration } from "./lifecycle-registry.js"; import type { AftProjectTransport, AftTransportOptions, AftTransportPool, ToolCallArguments, ToolCallOptions, ToolCallResult } from "./transport.js"; /** The subc pool is closing and cannot carry another request. */ export declare class SubcTransportShuttingDownError extends SubcCallError { constructor(); } /** True when subc-client rejected locally because its connection was already closed. */ export declare function isSubcClientClosedError(error: unknown): error is SubcError; /** A held-open event subscription — the slice of subc-client's Subscription we use. */ export interface SubcSubscriptionLike { /** Cancel the subscription (sends Cancel; idempotent); the provider unwinds with StreamEnd. */ unsubscribe(): void; /** Resolves on provider StreamEnd or unsubscribe; rejects on Error / route GOODBYE / socket drop. */ readonly closed: Promise; } /** * The minimal slice of {@link SubcClient} this transport depends on. Declared * structurally so a test can inject a fake client through the pool's `connect` * seam without standing up a daemon; the real `SubcClient` satisfies it. */ export interface SubcClientLike { routeOpen(target: RouteTarget, identity: BindIdentity, opts?: { consumerIdentity?: ConsumerIdentity | null; }): Promise; request(route: RouteHandle, body: unknown, opts?: RequestOptions): Promise; subscribe(route: RouteHandle, body: unknown, onEvent: (event: Uint8Array) => void): SubcSubscriptionLike; closeRouteChannel(route: RouteHandle, opts?: { drain?: boolean; }): Promise; /** Cumulative frames discarded because their route epoch did not match the client's current handle. */ readonly droppedIngressFrames?: number; close(): void; } export interface SubcTransportPoolOptions { /** Absolute path to the subc connection file (user-tier `subc.connection_file`). */ connectionFile: string; /** Harness identity carried in every BindIdentity ("opencode" | "pi" | …). */ harness: string; /** * Explicit route consumer identity. Undefined preserves subc-client's environment-derived * production identity; null is used by hermetic tests that run inside another module. */ consumerIdentity?: ConsumerIdentity | null; /** Handshake timeout forwarded to SubcClient.connect. */ handshakeTimeoutMs?: number; /** * Connection factory seam. Defaults to the real `SubcClient.connect`. Tests * inject a fake to exercise route caching / Rd reconnect without a daemon. */ connect?: (opts: { connectionFile: string; handshakeTimeoutMs?: number; }) => Promise; /** * Called when an idle bg-completion WAKE arrives for `(projectRoot, session)` * (a `{op:"bg_events"}` StreamData nudge), AND immediately after each * (re)subscribe (the durable-outbox replay trigger). The nudge carries NO * payload — the handler MUST force a DRAIN (bash_drain_completions) to fetch * the actual completions. When set, the transport opens a dedicated bg_events * subscription per session and drives its reconnect independently of tool * calls (so an idle agent whose socket drops is still resubscribed + drained). * Absent ⇒ no bg subscriptions are opened. */ onBgEventsNudge?: (projectRoot: string, session: string) => void; /** Test seam: backoff sleeper for the bg resubscribe loop (default real timer). */ bgBackoffSleep?: (ms: number) => Promise; /** Test seam for the pooled delay before reopening a route after an unknown-channel reply. */ routeRetrySleep?: (ms: number) => Promise; /** Test-only polling interval for detecting frames silently discarded with a stale route epoch. */ bgDispatchProbeIntervalMs?: number; /** Optional lifecycle registry used for root tracking; omit it to retain legacy behavior. */ lifecycleRegistry?: LifecycleRegistry; /** Configuration-shaped alias used by construction sites that group lifecycle seams. */ lifecycle?: { registry?: LifecycleRegistry; reapingEnabled?: boolean; demandCheck?: (root: CanonicalRootPath, poolId: ConcretePoolId) => boolean | { readonly exists?: boolean; }; evictOuterFacade?: (root: CanonicalRootPath, generation: RootGeneration) => void; onEvent?: (event: LifecycleEvent) => void; }; /** Fixed registration switch; it is immutable for the registration lifetime. */ reapingEnabled?: boolean; /** Alias for lifecycleDemandCheck matching LifecycleRegistry's seam name. */ demandCheck?: (root: CanonicalRootPath, poolId: ConcretePoolId) => boolean | { readonly exists?: boolean; }; /** * Synchronous demand seam for the synchronous AftTransportPool interface. A * false result prevents a facade, session, and lifecycle root from being made. * Async demand callers can use getBridgeForDemand below. */ lifecycleDemandCheck?: (root: CanonicalRootPath, poolId: ConcretePoolId) => boolean | { readonly exists?: boolean; }; /** * Optional callback used by direct concrete-pool tests. The wrapper normally * supplies this callback when it binds the returned registration handle. */ evictOuterFacade?: (root: CanonicalRootPath, generation: RootGeneration) => void; /** Optional structured event sink for tests and host metrics. */ onLifecycleEvent?: (event: LifecycleEvent) => void; /** Nudge rejection events are separate from root lifecycle events. */ onBgNudgeRejected?: (event: SubcBgNudgeRejectedEvent) => void; /** Optional nudge callback that receives complete generation provenance. */ onBgEventsNudgeRef?: (ref: BgNudgeRef) => void; /** * A pre-created registration is useful when a wrapper owns registration order: * bind it only after the wrapper callback has been installed. */ lifecycleRegistration?: LifecyclePoolRegistration; } /** Complete provenance captured when a bg_events subscription is installed. */ export interface BgNudgeRef { readonly canonicalRoot: CanonicalRootPath; readonly session: string; readonly concretePoolId: ConcretePoolId; readonly generation: RootGeneration; } export interface SubcBgNudgeRejectedEvent { readonly type: "subc_bg_nudge_rejected"; readonly canonicalRoot: CanonicalRootPath; readonly session: string; readonly expectedGeneration: RootGeneration; readonly currentGeneration?: RootGeneration; readonly expectedConcretePoolId: ConcretePoolId; readonly currentConcretePoolId?: ConcretePoolId; } interface RootGenerationErrorFields { readonly canonicalRoot: CanonicalRootPath; readonly expectedGeneration: RootGeneration; readonly currentGeneration?: RootGeneration; readonly concretePoolId?: ConcretePoolId; readonly currentConcretePoolId?: ConcretePoolId; } /** Stable classification for a request that loses its root to coordinated reap. */ export declare class SubcRootReapedError extends Error implements RootGenerationErrorFields { readonly code: "root_reaped"; readonly name = "SubcRootReapedError"; readonly canonicalRoot: CanonicalRootPath; readonly expectedGeneration: RootGeneration; readonly currentGeneration?: RootGeneration; readonly concretePoolId?: ConcretePoolId; readonly currentConcretePoolId?: ConcretePoolId; constructor(fields: RootGenerationErrorFields); } /** Stable classification for an operation holding an older root generation. */ export declare class SubcRootGenerationExpiredError extends Error implements RootGenerationErrorFields { readonly code: "root_generation_expired"; readonly name = "SubcRootGenerationExpiredError"; readonly canonicalRoot: CanonicalRootPath; readonly expectedGeneration: RootGeneration; readonly currentGeneration?: RootGeneration; readonly concretePoolId?: ConcretePoolId; readonly currentConcretePoolId?: ConcretePoolId; constructor(fields: RootGenerationErrorFields); } /** A synchronous lifecycle demand check could not establish that the root exists. */ export declare class SubcRootDemandRequiredError extends Error { readonly code: "root_demand_required"; readonly canonicalRoot: CanonicalRootPath; constructor(root: CanonicalRootPath); } declare const identityKeyBrand: unique symbol; export type IdentityKey = string & { readonly [identityKeyBrand]: "IdentityKey"; }; /** * One project root's view onto the shared subc client. Holds per-root status * caches (mirroring BinaryBridge) and routes every call through the pool's single * client, opening+caching a route per `(root, harness, session)`. */ declare class SubcTransport implements AftProjectTransport { private readonly pool; private readonly projectRoot; private readonly generation?; private cachedStatus; constructor(pool: SubcTransportPool, projectRoot: CanonicalRootPath, generation?: RootGeneration | undefined); getCwd(): string; /** Generation provenance is intentionally observable for lifecycle tests and nudge wiring. */ getGeneration(): RootGeneration | undefined; getConcretePoolId(): ConcretePoolId | undefined; getCachedStatus(): StatusSnapshot | null; cacheStatusSnapshot(snapshot: StatusSnapshot): void; private identityFor; private assertCurrent; toolCall(sessionId: string | undefined, name: string, rawArgs?: ToolCallArguments, options?: ToolCallOptions): Promise; /** * Lifecycle / native-command path. Over subc there is no separate "native * command" channel — every command rides the tool_provider route as a * `{name, arguments}` Request and the module's gate decides validity (the 21 * core tools plus the `bash_drain_completions` / `bash_ack_completions` plumbing * allowlist). The bind session is taken from `params.session_id` so a * session-scoped command (drain/ack) reaches the matching route — the module * re-injects the BIND session over any body session, so the route identity is * what scopes it. `configure` is satisfied locally (binding is the configure). */ send(command: string, params?: Record, options?: AftTransportOptions): Promise>; private splitOptions; } /** * Route cache over one authenticated subc client. In lifecycle mode this class * also owns the concrete pool's root index; the registry owns timer and root * transition state, while this class owns sessions, routes, and subscriptions. */ export declare class SubcTransportPool implements AftTransportPool { readonly harness: string; private readonly connectionFile; private readonly handshakeTimeoutMs?; private readonly consumerIdentity; private readonly connectFn; private readonly onBgEventsNudge?; private readonly onBgEventsNudgeRef?; private readonly bgBackoffSleep; private readonly routeRetrySleep; private readonly bgDispatchProbeIntervalMs; private readonly lifecycleDemandCheck?; private readonly onLifecycleEvent?; private readonly onBgNudgeRejected?; private lifecycleRegistry?; private registryUsesPoolEventSink; private lifecycleRegistration; private outerFacadeEvictor; private client; /** Single-flight guard so concurrent first calls share one connect. */ private connecting; /** The growing delay for a safe, once-only route resend after route closure. */ private routeReopenRetryDelayMs; /** Concurrent route closures and route.open refusals wait for the same retry timer. */ private routeReopenRetry; /** Delay assigned to the shared retry timer while it is pending. */ private routeReopenRetryMs; /** Per-session records keyed by the opaque identity key. */ private readonly sessions; /** * The sole root-scoped session enumeration authority. Keys are opaque and are * removed by the same detacher that removes the corresponding session record. */ private readonly rootIndex; /** Roots whose route binds must stay dormant until their directories return. */ private readonly dormantRoots; /** Consecutive non-transient failures on the current pool-local client. */ private transportFailures; /** Concrete per-root facades, including their captured root generation. */ private readonly transports; private readonly generationRejections; private readonly nudgeDeliveryLogState; private readonly pendingRootCleanups; private shuttingDown; private editSlotSurvives; private editSlotSurvivesCaptured; constructor(options: SubcTransportPoolOptions); /** * Bind a wrapper-owned registration after its generation-matched eviction * callback has been installed. The handle is the only authority that may * deregister this concrete pool. */ bindLifecycleRegistration(registration: LifecyclePoolRegistration): void; /** Construction helper for wrappers that own the registration sequence. */ registerLifecyclePool(registry: LifecycleRegistry, options: LifecyclePoolRegistrationOptions): LifecyclePoolRegistration; /** Replace only the callback; registration identity and switch stay immutable. */ setOuterFacadeEvictor(evictOuterFacade: (root: CanonicalRootPath, generation: RootGeneration) => void): void; getConcretePoolId(): ConcretePoolId | undefined; getCurrentRootGeneration(root: CanonicalRootPath): RootGeneration | undefined; recordBgNudgeRejection(ref: BgNudgeRef): void; getLifecycleRegistration(): LifecyclePoolRegistration | null; getLifecycleRegistry(): LifecycleRegistry | undefined; static connectionAvailable(connectionFile: string): Promise; private canonicalRoot; /** * Check a root immediately before opening a route. The reclaim marker is only * an existence hint; a directory that has already returned wins over a stale * sibling marker, so the marker contents are never parsed. */ private rootCanAttach; private markRootDormant; private assertRootCanAttach; private lifecycleEnabled; private currentGeneration; private currentPoolId; private isCurrentLiveGeneration; private isRootTombstoned; private recordGenerationRejection; private generationExpiredError; private rootReapedError; assertFacadeCurrent(root: CanonicalRootPath, expectedGeneration: RootGeneration | undefined, boundary: string): void; private assertGeneration; private assertRecordLive; private synchronousDemand; private makeFacade; /** * Return a live facade. Legacy construction remains synchronous. Lifecycle * construction uses the injected synchronous demand seam so a returned facade * always has a registered root and captured generation. */ getBridge(projectRoot: string): SubcTransport; /** Async demand entry point for registries whose existence seam is asynchronous. */ getBridgeForDemand(projectRoot: string): Promise; /** Alias used by host construction code that calls the seam a demand operation. */ demandBridge(projectRoot: string): Promise; getActiveBridgeForRoot(projectRoot: string): SubcTransport | null; /** Non-creating lookup requiring the complete pool/root/generation provenance. */ getActiveBridgeForRootGeneration(ref: BgNudgeRef): SubcTransport | null; activeBridges(): SubcTransport[]; toolCall(projectRoot: string, runtime: { sessionID?: string; }, name: string, rawArgs?: ToolCallArguments, options?: ToolCallOptions): Promise; private getOrCreateSession; private isCurrentSession; private currentSessionForNudge; private nudgeRefFor; private logNudgeDelivery; private removeIndexMembership; private deleteSessionIfEmpty; /** * Atomically detaches one opaque identity. Nothing in this method awaits: it * is the sole owner transfer used by session close, root reap, and shutdown. */ private detachSession; private cleanupDetached; private isReapInduced; private annotateReapError; /** Open or reuse a route while guarding every lifecycle boundary. */ routeRequest(identity: BindIdentity, body: Record, timeoutMs?: number, onProgress?: RequestOptions["onProgress"], expectedGeneration?: RootGeneration, abortSignal?: AbortSignal): Promise; /** * Hold a restart burst behind one growing timer before reopening an unknown * route. The resend remains bounded to one attempt, while a successful resend * resets the next outage to the minimum delay. */ private waitForRouteReopenBackoff; private resetRouteReopenBackoff; private ensureClient; private routeHandle; private ensureBgSubscription; /** * Invalidate only routes owned by a dead client. Sessions remain indexed so a * replacement client can reconnect the same identity and its bg subscription. */ private dropClient; /** Synchronous concrete-facade eviction used by the registry coordinator. */ evictConcreteFacade(root: CanonicalRootPath, generation: RootGeneration): void; /** * Registry-owned coordinated close. All record/index/facade mutations happen * before cleanup promises are created; the registry has already tombstoned the * matching generation and evicted the wrapper facade before this is called. */ closeProjectRoot(root: CanonicalRootPath, generation: RootGeneration): Promise<{ tornDownSessionCount: number; tornDownFacadeCount: number; }>; /** Forward explicit lifecycle close requests to the registry-owned coordinator. */ requestProjectRootClose(root: CanonicalRootPath, generation: RootGeneration, cause?: "sweep" | "explicit"): Promise; /** * Subc reads config locally, but plugin registration facts are process state * and must accompany route requests because RouteBind has no such field. */ setConfigureOverride(key: string, value: unknown): void; getEditSlotSurvives(): boolean | undefined; /** No-op over subc: the daemon owns the live module's configure lifecycle. */ reconfigure(_projectRoot: string, _overrides: Record): Promise; /** No-op over subc: the daemon supervises the binary, not the plugin. */ replaceBinary(path: string): Promise; isShutdown(): boolean; shutdown(): Promise; closeSession(projectRoot: string, session: string): Promise; } /** * Resolve a background nudge exactly once by looking up the existing active * bridge without creating one or reviving a shut-down pool; hosts use this * function to acknowledge nudge handling. */ export declare function resolveBridgeForNudge(pool: AftTransportPool, ref: BgNudgeRef): AftProjectTransport; export {}; //# sourceMappingURL=subc-transport.d.ts.map