import { Injectable, type OnModuleInit } from '@nestjs/common'; import { HarnessRegistry } from '../harness/harness.registry.js'; import { isHarnessConversationServiceAvailable, type HarnessConversationServiceBinding, } from '../harness/harness.tokens.js'; import type { ChatRuntime, ChatRuntimeMode, LegacyBrowserMessagePayload, LegacyEmbeddedChatPort, LegacyRuntimeResult, LegacyRuntimeStream, LegacySessionPresentation, LegacySocketTurnLease, OwnedConversationContext, VerifiedDiscordIngressContext, VerifiedDiscordTurnLease, } from './chat-runtime.js'; import { ChatRuntimeUnavailableError, resolveChatRuntimeMode } from './chat-runtime.js'; /** The fixed fail-closed result for a legacy browser operation issued under `pi-rpc`. */ const RUNTIME_UNSUPPORTED = { ok: false as const, code: 'runtime_unsupported' as const, retryable: false as const, }; /** * Resolves the one live {@link ChatRuntime} for this process and enforces the * `pi-rpc` readiness preconditions at module init — before the gateway accepts * traffic. It never falls back from `pi-rpc` to embedded execution: an unmet * `pi-rpc` precondition is a typed startup failure ({@link ChatRuntimeUnavailableError}), * and until `onModuleInit` selects a runtime, {@link active} throws rather than * exposing any runtime — a failed `pi-rpc` init can never leak the embedded one. */ @Injectable() export class ChatRuntimeRouter implements OnModuleInit, LegacyEmbeddedChatPort { private readonly mode: ChatRuntimeMode; /** The single resolved runtime. Undefined until a successful `onModuleInit`. */ private resolved: ChatRuntime | undefined; constructor( private readonly harnessRegistry: HarnessRegistry, private readonly conversationService: HarnessConversationServiceBinding, private readonly embedded: ChatRuntime, private readonly harness: ChatRuntime, mode: ChatRuntimeMode = resolveChatRuntimeMode(), ) { this.mode = mode; } onModuleInit(): void { if (this.mode === 'legacy') { // Legacy ignores the pi-rpc preconditions entirely and always runs embedded. this.resolved = this.embedded; return; } // pi-rpc: both preconditions are hard startup failures, checked in a fixed order. if (!this.harnessRegistry.has('pi')) { this.resolved = undefined; throw new ChatRuntimeUnavailableError('adapter_unavailable'); } if (!isHarnessConversationServiceAvailable(this.conversationService)) { this.resolved = undefined; throw new ChatRuntimeUnavailableError('conversation_service_unavailable'); } this.resolved = this.harness; } get active(): ChatRuntime { if (this.resolved === undefined) { // Reached only if init has not run or failed closed; never expose a runtime here. throw new Error('The chat runtime is not available: startup did not resolve a runtime.'); } return this.resolved; } /** * The process-wide mode, available before {@link onModuleInit}. Production handlers read * this to fail a legacy browser turn closed under `pi-rpc` *before* parsing the payload as * either browser-legacy input or a Discord envelope — never to branch into a fallback. */ get runtimeMode(): ChatRuntimeMode { return this.mode; } /** * The embedded runtime narrowed to its port. Only reached on the legacy path (and for the * verified-Discord op in both modes), where the injected runtime is always a real * `EmbeddedChatRuntime`. The router spec constructs the router with a bare `{ kind }` stub * but never invokes a port op, so this narrowing is never exercised against the stub. */ private get embeddedPort(): LegacyEmbeddedChatPort { return this.embedded as unknown as LegacyEmbeddedChatPort; } // --- LegacyEmbeddedChatPort: legacy browser operations fail closed under pi-rpc --- completeLegacyRestTurn( context: OwnedConversationContext, input: Readonly<{ content: string }>, ): Promise< LegacyRuntimeResult> > { if (this.mode === 'pi-rpc') { return Promise.resolve(RUNTIME_UNSUPPORTED); } return this.embeddedPort.completeLegacyRestTurn(context, input); } prepareLegacySocketTurn( context: OwnedConversationContext, input: LegacyBrowserMessagePayload, stream: LegacyRuntimeStream, ): Promise> { if (this.mode === 'pi-rpc') { return Promise.resolve(RUNTIME_UNSUPPORTED); } return this.embeddedPort.prepareLegacySocketTurn(context, input, stream); } setLegacyThinking( context: OwnedConversationContext, level: string, ): LegacyRuntimeResult { if (this.mode === 'pi-rpc') { return RUNTIME_UNSUPPORTED; } return this.embeddedPort.setLegacyThinking(context, level); } abortLegacyTurn(context: OwnedConversationContext): Promise> { if (this.mode === 'pi-rpc') { return Promise.resolve(RUNTIME_UNSUPPORTED); } return this.embeddedPort.abortLegacyTurn(context); } applyLegacyModelOverride( context: OwnedConversationContext, modelId: string, ): LegacyRuntimeResult { if (this.mode === 'pi-rpc') { return RUNTIME_UNSUPPORTED; } return this.embeddedPort.applyLegacyModelOverride(context, modelId); } readLegacySessionPresentation( context: OwnedConversationContext, ): LegacyRuntimeResult { if (this.mode === 'pi-rpc') { return RUNTIME_UNSUPPORTED; } return this.embeddedPort.readLegacySessionPresentation(context); } /** * Verified Discord ingress bypasses browser mode: it is embedded-only in BOTH modes and * never reaches the harness or routing-engine selection. It is reached only through a * {@link VerifiedDiscordIngressContext}, which exists only after every ingress check. */ dispatchVerifiedDiscordIngress( context: VerifiedDiscordIngressContext, stream: LegacyRuntimeStream, ): Promise> { return this.embeddedPort.dispatchVerifiedDiscordIngress(context, stream); } }