Download packages/coding-agent/src/experimental/server.ts from SaylorTwift/pi: direct link, hf CLI and curl.
- Browser
- Download file 29.9 kB
-
https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/src/experimental/server.ts
- Command line
-
hf download hf://SaylorTwift/pi/packages/coding-agent/src/experimental/server.ts
-
curl -L -o server.ts https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/src/experimental/server.ts
29.9 kB
| import { randomUUID } from "node:crypto"; | |
| import { chmod, lstat, mkdir, readFile, writeFile } from "node:fs/promises"; | |
| import { homedir } from "node:os"; | |
| import { isAbsolute, join } from "node:path"; | |
| import type { Context } from "@earendil-works/chord"; | |
| import type { FacetBundleArtifact } from "@earendil-works/chord/node"; | |
| import { | |
| BACKGROUND_CONTEXT, | |
| type JsonlSessionMetadata, | |
| JsonlSessionRepo, | |
| TODO_CONTEXT, | |
| } from "@earendil-works/pi-agent-core"; | |
| import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node"; | |
| import { Client, ServerError as ClientServerError, DisconnectedError } from "@earendil-works/pi-client"; | |
| import { createUnixTransportFactory, type UnixServerRoute } from "@earendil-works/pi-client/unix"; | |
| import { isServerId, type ServerId } from "@earendil-works/pi-protocol"; | |
| import { | |
| ServerError as RoutedServerError, | |
| type Server, | |
| type ServerHost, | |
| SessionAmbiguousError, | |
| SessionNotFoundError, | |
| } from "@earendil-works/pi-server"; | |
| import { createUnixServer, getUnixSocketPath } from "@earendil-works/pi-server/unix"; | |
| import lockfile from "proper-lockfile"; | |
| import type { AuthInput } from "../cli/experimental/command-options.ts"; | |
| import { getAgentDir } from "../config.ts"; | |
| import { resolvePath } from "../utils/paths.ts"; | |
| import { CoordinatorConnection, type CoordinatorStartupLease, ensureCoordinator } from "./coordinator.ts"; | |
| import { createPresentationFacetData } from "./plugins/bundled.ts"; | |
| import { | |
| createServerPluginPackage, | |
| normalizePluginPackagePaths, | |
| readSessionPluginPackageProfile, | |
| removeSessionPluginPackageProfile, | |
| restoreServerPluginPackageProfile, | |
| writeSessionPluginPackageProfile, | |
| } from "./plugins/package.ts"; | |
| import { | |
| consumeInternalProcessRole, | |
| isDirectInternalProcessEntry, | |
| spawnInternalProcess, | |
| terminateInternalProcess, | |
| } from "./process.ts"; | |
| import { RadiusRelayAuthResolver } from "./radius-auth.ts"; | |
| import { RadiusRelayHost, type RadiusRelayHostStatus } from "./radius-relay.ts"; | |
| import { createExperimentalServerServices } from "./services/server.ts"; | |
| import type { SessionCreateOptions, SessionSummary } from "./services/sessions.ts"; | |
| import { SessionPluginSelectionConflictError, SessionWorkerManager } from "./session-worker-manager.ts"; | |
| export const ENV_SERVER_DIR = "PI_SERVER_DIR"; | |
| export const ENV_SERVER_ID = "PI_SERVER_ID"; | |
| export function resolveServerDirectory(directory?: string): string { | |
| return resolvePath(directory ?? process.env[ENV_SERVER_DIR] ?? join(homedir(), ".pi", "server")); | |
| } | |
| export async function ensurePrivateServerDirectory(directory: string): Promise<void> { | |
| if (typeof process.getuid !== "function") throw new Error("Unix socket directory requires a POSIX user ID"); | |
| await mkdir(directory, { recursive: true, mode: 0o700 }); | |
| const stats = await lstat(directory); | |
| if (!stats.isDirectory()) throw new Error(`Unix socket directory is not a directory: ${directory}`); | |
| if (stats.uid !== process.getuid()) { | |
| throw new Error(`Unix socket directory is not owned by the current user: ${directory}`); | |
| } | |
| await chmod(directory, 0o700); | |
| } | |
| export function resolveSessionDirectory(sessionDir?: string): string { | |
| return resolvePath(sessionDir ?? join(getAgentDir(), "experimental", "sessions")); | |
| } | |
| const LOCK_STALE_MS = 30_000; | |
| const LOCK_RETRY_MS = 25; | |
| const LOCK_WAIT_MS = 30_000; | |
| const DEFAULT_SERVER_ID_FILE = "default-server-id"; | |
| export interface ServerProfile { | |
| readonly serverId: ServerId; | |
| release(): Promise<void>; | |
| } | |
| /** Lock one logical server ID in a shared experimental server directory. */ | |
| export async function acquireServerProfile(directory: string, requestedServerId?: string): Promise<ServerProfile> { | |
| await mkdir(directory, { recursive: true, mode: 0o700 }); | |
| let serverId: ServerId; | |
| if (requestedServerId !== undefined) { | |
| if (!isServerId(requestedServerId)) throw new Error(`Invalid experimental server ID: ${requestedServerId}`); | |
| serverId = requestedServerId; | |
| } else { | |
| const path = join(directory, DEFAULT_SERVER_ID_FILE); | |
| try { | |
| const value = (await readFile(path, "utf8")).trim(); | |
| if (!isServerId(value)) throw new Error(`Invalid default experimental server identity in ${path}`); | |
| serverId = value; | |
| } catch (error) { | |
| const code = error instanceof Error && "code" in error ? error.code : undefined; | |
| if (code !== "ENOENT") throw error; | |
| const candidate = randomUUID(); | |
| try { | |
| await writeFile(path, candidate, { encoding: "utf8", mode: 0o600, flag: "wx" }); | |
| serverId = candidate; | |
| } catch (writeError) { | |
| const writeCode = writeError instanceof Error && "code" in writeError ? writeError.code : undefined; | |
| if (writeCode !== "EEXIST") throw writeError; | |
| const value = (await readFile(path, "utf8")).trim(); | |
| if (!isServerId(value)) throw new Error(`Invalid default experimental server identity in ${path}`); | |
| serverId = value; | |
| } | |
| } | |
| } | |
| const release = await lockfile.lock(join(directory, `launcher-${serverId}`), { | |
| realpath: false, | |
| stale: LOCK_STALE_MS, | |
| update: LOCK_STALE_MS / 3, | |
| retries: { | |
| retries: Math.ceil(LOCK_WAIT_MS / LOCK_RETRY_MS), | |
| factor: 1, | |
| minTimeout: LOCK_RETRY_MS, | |
| maxTimeout: LOCK_RETRY_MS, | |
| maxRetryTime: LOCK_WAIT_MS, | |
| }, | |
| }); | |
| return { serverId, release }; | |
| } | |
| const ACTIVATION_TIMEOUT_MS = 10_000; | |
| const ACTIVATION_RETRY_MS = 10; | |
| export interface ActivatedServer { | |
| readonly client: Client; | |
| readonly route: UnixServerRoute; | |
| } | |
| export interface ActivateServerOptions { | |
| readonly directory: string; | |
| readonly requestedServerId?: ServerId | string; | |
| readonly sessionDir: string; | |
| readonly provider?: string; | |
| readonly model?: string; | |
| } | |
| /** Ensure the selected logical server is reachable, launching the current Pi installation if needed. */ | |
| export async function activateServer(options: ActivateServerOptions): Promise<ActivatedServer> { | |
| if (options.provider !== undefined && options.model === undefined) { | |
| throw new Error("Server model provider requires a model"); | |
| } | |
| await ensurePrivateServerDirectory(options.directory); | |
| const profile = await acquireServerProfile(options.directory, options.requestedServerId); | |
| const serverId = profile.serverId; | |
| await profile.release(); | |
| const route = { serverId, path: getUnixSocketPath(serverId, options.directory) }; | |
| const release = await acquireServerActivation(options.directory, serverId); | |
| try { | |
| const existing = await connect(route); | |
| if (existing) { | |
| // Another activator won the race, so startup-only selections can no longer be applied. | |
| if (options.model !== undefined) { | |
| await existing.dispose(); | |
| throw new Error("Model selection is only valid when automatically activating a new server"); | |
| } | |
| return { client: existing, route }; | |
| } | |
| const modelArgs = | |
| options.model === undefined | |
| ? [] | |
| : [ | |
| JSON.stringify({ | |
| ...(options.provider === undefined ? {} : { provider: options.provider }), | |
| model: options.model, | |
| }), | |
| ]; | |
| const child = spawnInternalProcess("server", [options.directory, serverId, options.sessionDir, ...modelArgs]); | |
| let spawnError: Error | undefined; | |
| child.once("error", (error) => { | |
| spawnError = error; | |
| }); | |
| try { | |
| const deadline = Date.now() + ACTIVATION_TIMEOUT_MS; | |
| while (true) { | |
| const client = await connect(route); | |
| if (client) return { client, route }; | |
| if (spawnError) throw new Error("Failed to automatically activate server", { cause: spawnError }); | |
| if (child.exitCode !== null || child.signalCode !== null) { | |
| throw new Error("Automatically activated server exited during startup"); | |
| } | |
| if (Date.now() >= deadline) throw new Error("Timed out waiting for automatically activated server"); | |
| await new Promise<void>((resolve) => setTimeout(resolve, ACTIVATION_RETRY_MS)); | |
| } | |
| } catch (error) { | |
| await terminateInternalProcess(child); | |
| throw error; | |
| } | |
| } finally { | |
| await release(); | |
| } | |
| } | |
| export function acquireServerActivation(directory: string, serverId: ServerId): Promise<() => Promise<void>> { | |
| return lockfile.lock(join(directory, `activation-${serverId}`), { | |
| realpath: false, | |
| stale: ACTIVATION_TIMEOUT_MS * 2, | |
| update: ACTIVATION_TIMEOUT_MS, | |
| retries: { | |
| retries: Math.ceil(ACTIVATION_TIMEOUT_MS / 25), | |
| factor: 1, | |
| minTimeout: 25, | |
| maxTimeout: 25, | |
| maxRetryTime: ACTIVATION_TIMEOUT_MS, | |
| }, | |
| }); | |
| } | |
| async function connect(route: UnixServerRoute): Promise<Client | undefined> { | |
| const client = new Client({ | |
| serverId: route.serverId, | |
| transportFactory: createUnixTransportFactory({ path: route.path }), | |
| }); | |
| try { | |
| await client.connect(); | |
| return client; | |
| } catch (error) { | |
| await client.dispose(); | |
| if (error instanceof DisconnectedError || (error instanceof ClientServerError && error.code === "version")) { | |
| return undefined; | |
| } | |
| let current = error; | |
| const seen = new Set<unknown>(); | |
| while (current instanceof Error && !seen.has(current)) { | |
| seen.add(current); | |
| if ( | |
| "code" in current && | |
| ["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE", "ETIMEDOUT"].includes(String(current.code)) | |
| ) { | |
| return undefined; | |
| } | |
| current = current.cause; | |
| } | |
| throw error; | |
| } | |
| } | |
| const AUTO_SERVER_STARTUP_GRACE_MS = 10_000; | |
| const AUTO_SERVER_IDLE_GRACE_MS = 1_000; | |
| /** Reconcile operator, startup, client, and worker holds for one server generation. */ | |
| export class ServerLifetime { | |
| readonly #keepAlive: boolean; | |
| #connectionCount = 0; | |
| #workerCount = 0; | |
| #startupHeld: boolean; | |
| #startupTimer: NodeJS.Timeout | undefined; | |
| #retirementTimer: NodeJS.Timeout | undefined; | |
| #retire: (() => void) | undefined; | |
| #stopped = false; | |
| constructor(keepAlive: boolean) { | |
| this.#keepAlive = keepAlive; | |
| this.#startupHeld = !keepAlive; | |
| } | |
| start(retire: () => void): void { | |
| this.#retire = retire; | |
| if (this.#startupHeld) { | |
| this.#startupTimer = setTimeout(() => { | |
| this.#startupTimer = undefined; | |
| this.#startupHeld = false; | |
| this.#reconcile(); | |
| }, AUTO_SERVER_STARTUP_GRACE_MS); | |
| this.#startupTimer.unref(); | |
| } | |
| this.#reconcile(); | |
| } | |
| setConnectionCount(count: number): void { | |
| this.#connectionCount = count; | |
| if (count > 0 && this.#startupHeld) { | |
| this.#startupHeld = false; | |
| if (this.#startupTimer) clearTimeout(this.#startupTimer); | |
| this.#startupTimer = undefined; | |
| } | |
| this.#reconcile(); | |
| } | |
| setWorkerCount(count: number): void { | |
| this.#workerCount = count; | |
| this.#reconcile(); | |
| } | |
| stop(): void { | |
| this.#stopped = true; | |
| if (this.#startupTimer) clearTimeout(this.#startupTimer); | |
| if (this.#retirementTimer) clearTimeout(this.#retirementTimer); | |
| this.#startupTimer = undefined; | |
| this.#retirementTimer = undefined; | |
| } | |
| #reconcile(): void { | |
| if ( | |
| this.#stopped || | |
| this.#keepAlive || | |
| this.#startupHeld || | |
| this.#connectionCount !== 0 || | |
| this.#workerCount !== 0 | |
| ) { | |
| if (this.#retirementTimer) clearTimeout(this.#retirementTimer); | |
| this.#retirementTimer = undefined; | |
| return; | |
| } | |
| const retire = this.#retire; | |
| if (this.#retirementTimer || !retire) return; | |
| this.#retirementTimer = setTimeout(() => { | |
| this.#retirementTimer = undefined; | |
| if (!this.#stopped && !this.#startupHeld && this.#connectionCount === 0 && this.#workerCount === 0) { | |
| retire(); | |
| } | |
| }, AUTO_SERVER_IDLE_GRACE_MS); | |
| this.#retirementTimer.unref(); | |
| } | |
| } | |
| export interface RunningServer { | |
| readonly serverId: string; | |
| readonly sessionDir: string; | |
| readonly socketPath: string; | |
| readonly server: Server; | |
| readonly workerPids: ReadonlyMap<string, number>; | |
| readonly closed: Promise<void>; | |
| close(): Promise<void>; | |
| } | |
| export interface StartServerOptions { | |
| /** Server profile and socket directory. Defaults to PI_SERVER_DIR or ~/.pi/server. */ | |
| readonly directory?: string; | |
| /** Logical service ID. Defaults to PI_SERVER_ID or the directory's default-server-id. */ | |
| readonly serverId?: ServerId; | |
| /** Durable session directory. Defaults to the experimental directory under the configured agent directory. */ | |
| readonly sessionDir?: string; | |
| /** Optional provider for an explicitly selected Session worker model. */ | |
| readonly provider?: string; | |
| /** Optional model override for newly started Session workers. */ | |
| readonly model?: string; | |
| /** Hold the server open without client or Session demand. Defaults to true for foreground servers. */ | |
| readonly keepAlive?: boolean; | |
| /** Optional explicit Radius credential. Stored Radius auth is used when omitted. */ | |
| readonly relayAuth?: AuthInput; | |
| /** Explicit plugin packages. Undefined restores the logical server profile; an empty list clears it. */ | |
| readonly pluginPackages?: readonly string[]; | |
| readonly onRelayStatus?: (status: RadiusRelayHostStatus) => void; | |
| } | |
| interface ResolvedSessionPlugins { | |
| readonly packagePaths: readonly string[]; | |
| readonly manifestPaths: readonly string[]; | |
| readonly presentationArtifacts: readonly FacetBundleArtifact[]; | |
| } | |
| interface StartServerBackendOptions { | |
| readonly path: string; | |
| readonly serverId: ServerId; | |
| readonly sessionDir?: string; | |
| resolveSessionPlugins( | |
| metadata: JsonlSessionMetadata, | |
| packagePaths: readonly string[] | undefined, | |
| context: Context, | |
| ): Promise<ResolvedSessionPlugins>; | |
| removeSessionPlugins(metadata: JsonlSessionMetadata): Promise<void>; | |
| reloadPresentationFacetBundles(packagePaths: readonly string[]): Promise<readonly FacetBundleArtifact[]>; | |
| } | |
| interface RunningServerBackend extends RunningServer { | |
| refreshSessions(): Promise<void>; | |
| } | |
| async function startServerBackend( | |
| options: StartServerBackendOptions, | |
| workers: SessionWorkerManager, | |
| onConnectionCountChanged?: (count: number) => void, | |
| ): Promise<RunningServerBackend> { | |
| const serverId = options.serverId; | |
| const sessionDir = resolveSessionDirectory(options.sessionDir); | |
| const executionEnv = new NodeExecutionEnv({ cwd: process.cwd() }); | |
| const repo = new JsonlSessionRepo({ fileSystem: executionEnv, sessionsRoot: sessionDir }); | |
| const listSessions = async (context: Context): Promise<JsonlSessionMetadata[]> => { | |
| const sessions = new Map((await repo.list(undefined, context)).map((metadata) => [metadata.path, metadata])); | |
| for (const metadata of workers.trackedSessions) sessions.set(metadata.path, metadata); | |
| return [...sessions.values()]; | |
| }; | |
| const resolveSession = async (sessionId: string, context: Context): Promise<JsonlSessionMetadata> => { | |
| const matches = (await listSessions(context)).filter((metadata) => metadata.id === sessionId); | |
| if (matches.length === 0) throw new SessionNotFoundError(`Unknown session: ${sessionId}`); | |
| if (matches.length > 1) throw new SessionAmbiguousError(); | |
| return matches[0]!; | |
| }; | |
| const createSession = async ( | |
| createOptions: SessionCreateOptions, | |
| context: Context, | |
| ): Promise<JsonlSessionMetadata> => { | |
| const session = await repo.create({ ...createOptions, cwd: process.cwd() }, context); | |
| try { | |
| return session.metadata; | |
| } finally { | |
| await session.close(context); | |
| } | |
| }; | |
| const summarize = (metadata: JsonlSessionMetadata): SessionSummary => ({ | |
| serverId, | |
| sessionId: metadata.id, | |
| createdAt: metadata.createdAt, | |
| }); | |
| const closeStorage = async (): Promise<void> => { | |
| const cleanup = await Promise.allSettled([repo.close(TODO_CONTEXT), executionEnv.cleanup(TODO_CONTEXT)]); | |
| const errors = cleanup.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])); | |
| if (errors.length === 1) throw errors[0]; | |
| if (errors.length > 1) throw new AggregateError(errors, "Experimental session storage cleanup failed"); | |
| }; | |
| const serverServices = await createExperimentalServerServices({ | |
| list: async (context) => | |
| (await listSessions(context)) | |
| .map(summarize) | |
| .sort((left, right) => left.sessionId.localeCompare(right.sessionId) || left.createdAt - right.createdAt), | |
| create: async (createOptions, context) => summarize(await createSession(createOptions, context)), | |
| remove: async (sessionId, context) => { | |
| const metadata = await resolveSession(sessionId, context); | |
| await workers.closeSession(metadata, context); | |
| await repo.delete(metadata, context); | |
| await options.removeSessionPlugins(metadata); | |
| }, | |
| async prepareSessionPlugins(sessionId, packagePaths, context) { | |
| const metadata = await resolveSession(sessionId, context); | |
| let selected: ResolvedSessionPlugins; | |
| try { | |
| selected = await options.resolveSessionPlugins(metadata, packagePaths, context); | |
| workers.assertSessionPluginManifestPaths(metadata, selected.manifestPaths); | |
| } catch (error) { | |
| if (error instanceof SessionPluginSelectionConflictError) { | |
| throw new RoutedServerError("service_invalid_value", error.message); | |
| } | |
| throw error; | |
| } | |
| return { | |
| packagePaths: selected.packagePaths, | |
| presentationPlugins: createPresentationFacetData(selected.presentationArtifacts), | |
| }; | |
| }, | |
| async reloadPresentationPlugins(packagePaths) { | |
| return createPresentationFacetData(await options.reloadPresentationFacetBundles(packagePaths)); | |
| }, | |
| }).catch(async (error: unknown) => { | |
| try { | |
| await closeStorage(); | |
| } catch (cleanupError) { | |
| throw new AggregateError([error, cleanupError], "Server service startup and cleanup failed"); | |
| } | |
| throw error; | |
| }); | |
| const host: ServerHost<JsonlSessionMetadata> = { | |
| serverServices: serverServices.host, | |
| resolveSession, | |
| openSession: async (metadata, context) => { | |
| const selected = await options.resolveSessionPlugins(metadata, undefined, context); | |
| return workers.openSession(metadata, context, selected.manifestPaths); | |
| }, | |
| }; | |
| const socketPath = options.path; | |
| const closeCatalog = async (): Promise<void> => { | |
| const cleanup = await Promise.allSettled([serverServices.dispose(), closeStorage()]); | |
| const errors = cleanup.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])); | |
| if (errors.length === 1) throw errors[0]; | |
| if (errors.length > 1) throw new AggregateError(errors, "Experimental session catalog cleanup failed"); | |
| }; | |
| const server = createUnixServer(host, { | |
| serverId, | |
| path: socketPath, | |
| mode: 0o600, | |
| onConnectionCountChanged, | |
| }); | |
| try { | |
| await server.start(); | |
| } catch (error) { | |
| const cleanup = await Promise.allSettled([server.close(), closeCatalog()]); | |
| const cleanupErrors = cleanup.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])); | |
| if (cleanupErrors.length > 0) { | |
| throw new AggregateError([error, ...cleanupErrors], "Experimental server startup and cleanup failed"); | |
| } | |
| throw error; | |
| } | |
| let closePromise: Promise<void> | undefined; | |
| const closed = server.closed.then( | |
| () => closeCatalog(), | |
| async (serverError: unknown) => { | |
| try { | |
| await closeCatalog(); | |
| } catch (repoError) { | |
| throw new AggregateError([serverError, repoError], "Server and repository shutdown failed"); | |
| } | |
| throw serverError; | |
| }, | |
| ); | |
| return { | |
| serverId, | |
| sessionDir, | |
| socketPath, | |
| server, | |
| workerPids: workers.workerPids, | |
| closed, | |
| refreshSessions: () => serverServices.refresh(BACKGROUND_CONTEXT), | |
| close() { | |
| closePromise ??= server.close().then( | |
| () => closed, | |
| () => closed, | |
| ); | |
| return closePromise; | |
| }, | |
| }; | |
| } | |
| /** Start a replaceable experimental server behind the stable coordinator endpoint. */ | |
| export async function startServer(options: StartServerOptions = {}): Promise<RunningServer> { | |
| if (options.provider !== undefined && options.model === undefined) { | |
| throw new Error("Server model provider requires a model"); | |
| } | |
| const workerModel = | |
| options.model === undefined | |
| ? undefined | |
| : { ...(options.provider === undefined ? {} : { provider: options.provider }), model: options.model }; | |
| const directory = resolveServerDirectory(options.directory); | |
| const { serverId, release } = await acquireServerProfile(directory, options.serverId ?? process.env[ENV_SERVER_ID]); | |
| const lifetime = new ServerLifetime(options.keepAlive ?? true); | |
| let backend: RunningServerBackend | undefined; | |
| let coordinator: CoordinatorConnection | undefined; | |
| let startupLease: CoordinatorStartupLease | undefined; | |
| let workers: SessionWorkerManager | undefined; | |
| let relay: RadiusRelayHost | undefined; | |
| let released = false; | |
| try { | |
| await ensurePrivateServerDirectory(directory); | |
| const pluginPackagePaths = await restoreServerPluginPackageProfile(directory, serverId, options.pluginPackages); | |
| const pluginPackages = new Map<string, ReturnType<typeof createServerPluginPackage>>(); | |
| const getPluginPackage = (packagePath: string): ReturnType<typeof createServerPluginPackage> => { | |
| let plugin = pluginPackages.get(packagePath); | |
| if (plugin === undefined) { | |
| plugin = createServerPluginPackage(directory, serverId, packagePath); | |
| pluginPackages.set(packagePath, plugin); | |
| } | |
| return plugin; | |
| }; | |
| const buildPluginSelection = async (packagePaths: readonly string[]): Promise<ResolvedSessionPlugins> => { | |
| const normalizedPackagePaths = normalizePluginPackagePaths(packagePaths); | |
| const plugins = normalizedPackagePaths.map(getPluginPackage); | |
| const built = (await Promise.all(plugins.map((plugin) => plugin.build()))).flat(); | |
| return { | |
| packagePaths: normalizedPackagePaths, | |
| manifestPaths: plugins.map((plugin) => plugin.manifestPath), | |
| presentationArtifacts: built, | |
| }; | |
| }; | |
| let defaultPluginSelection = await buildPluginSelection(pluginPackagePaths); | |
| const sessionPluginSelections = new Map<string, ResolvedSessionPlugins>(); | |
| const resolveSessionPlugins = async ( | |
| metadata: JsonlSessionMetadata, | |
| requestedPackagePaths: readonly string[] | undefined, | |
| _context: Context, | |
| ): Promise<ResolvedSessionPlugins> => { | |
| if (requestedPackagePaths !== undefined) { | |
| const normalizedPackagePaths = normalizePluginPackagePaths(requestedPackagePaths); | |
| const current = sessionPluginSelections.get(metadata.path); | |
| if (current !== undefined && sameStrings(current.packagePaths, normalizedPackagePaths)) return current; | |
| const requestedManifestPaths = normalizedPackagePaths.map( | |
| (packagePath) => getPluginPackage(packagePath).manifestPath, | |
| ); | |
| workers?.assertSessionPluginManifestPaths(metadata, requestedManifestPaths); | |
| const candidate = await buildPluginSelection(normalizedPackagePaths); | |
| workers?.assertSessionPluginManifestPaths(metadata, candidate.manifestPaths); | |
| await writeSessionPluginPackageProfile(directory, serverId, metadata.path, candidate.packagePaths); | |
| sessionPluginSelections.set(metadata.path, candidate); | |
| return candidate; | |
| } | |
| const cached = sessionPluginSelections.get(metadata.path); | |
| if (cached !== undefined) return cached; | |
| const storedPackagePaths = await readSessionPluginPackageProfile(directory, serverId, metadata.path); | |
| const selected = | |
| storedPackagePaths === undefined ? defaultPluginSelection : await buildPluginSelection(storedPackagePaths); | |
| if (storedPackagePaths === undefined) { | |
| await writeSessionPluginPackageProfile(directory, serverId, metadata.path, selected.packagePaths); | |
| } | |
| sessionPluginSelections.set(metadata.path, selected); | |
| return selected; | |
| }; | |
| const removeSessionPlugins = async (metadata: JsonlSessionMetadata): Promise<void> => { | |
| sessionPluginSelections.delete(metadata.path); | |
| await removeSessionPluginPackageProfile(directory, serverId, metadata.path); | |
| }; | |
| const reloadPresentationFacetBundles = async ( | |
| packagePaths: readonly string[], | |
| ): Promise<readonly FacetBundleArtifact[]> => { | |
| const reloaded = await buildPluginSelection(packagePaths); | |
| if (sameStrings(defaultPluginSelection.packagePaths, reloaded.packagePaths)) { | |
| defaultPluginSelection = reloaded; | |
| } | |
| for (const [sessionPath, selected] of sessionPluginSelections) { | |
| if (sameStrings(selected.packagePaths, reloaded.packagePaths)) { | |
| sessionPluginSelections.set(sessionPath, reloaded); | |
| } | |
| } | |
| return reloaded.presentationArtifacts; | |
| }; | |
| const socketPath = getUnixSocketPath(serverId, directory); | |
| const controlPath = join(directory, `control-${serverId}.sock`); | |
| const serverNonce = randomUUID().replaceAll("-", "").slice(0, 12); | |
| const serverPath = join(directory, `server-${serverId}-${serverNonce}.sock`); | |
| startupLease = await ensureCoordinator(socketPath, controlPath); | |
| coordinator = new CoordinatorConnection({ controlPath, endpoint: serverPath }); | |
| const sessionDir = resolveSessionDirectory(options.sessionDir); | |
| workers = new SessionWorkerManager(coordinator, sessionDir, workerModel, (count) => | |
| lifetime.setWorkerCount(count), | |
| ); | |
| backend = await startServerBackend( | |
| { | |
| path: serverPath, | |
| serverId, | |
| sessionDir: options.sessionDir, | |
| resolveSessionPlugins, | |
| removeSessionPlugins, | |
| reloadPresentationFacetBundles, | |
| }, | |
| workers, | |
| (count) => lifetime.setConnectionCount(count), | |
| ); | |
| await coordinator.connect(); | |
| startupLease.close(); | |
| startupLease = undefined; | |
| await workers.discover(coordinator.peerIds); | |
| await backend.refreshSessions(); | |
| relay = new RadiusRelayHost({ | |
| serverId, | |
| server: backend.server, | |
| auth: new RadiusRelayAuthResolver(options.relayAuth), | |
| onStatus: options.onRelayStatus, | |
| }); | |
| relay.start(); | |
| const activeBackend = backend; | |
| const activeCoordinator = coordinator; | |
| const activeWorkers = workers; | |
| const activeRelay = relay; | |
| void activeCoordinator.replaced | |
| .then(async () => { | |
| lifetime.stop(); | |
| activeWorkers.detach(); | |
| await activeRelay.close(); | |
| await activeBackend.close(); | |
| }) | |
| .finally(() => activeCoordinator.close()) | |
| .catch(() => {}); | |
| let closePromise: Promise<void> | undefined; | |
| const runtime: RunningServer = { | |
| serverId, | |
| sessionDir: activeBackend.sessionDir, | |
| socketPath, | |
| server: activeBackend.server, | |
| workerPids: activeWorkers.workerPids, | |
| closed: activeBackend.closed.finally(() => activeRelay.close()), | |
| close() { | |
| lifetime.stop(); | |
| closePromise ??= (async () => { | |
| try { | |
| await activeRelay.close(); | |
| await activeBackend.close(); | |
| } finally { | |
| try { | |
| if (activeCoordinator.wasReplaced) activeWorkers.detach(); | |
| else await activeWorkers.shutdown(); | |
| } finally { | |
| activeCoordinator.close(); | |
| } | |
| } | |
| })(); | |
| return closePromise; | |
| }, | |
| }; | |
| lifetime.start(() => { | |
| void runtime.close().catch(() => {}); | |
| }); | |
| released = true; | |
| await release(); | |
| return runtime; | |
| } catch (error) { | |
| lifetime.stop(); | |
| startupLease?.close(); | |
| if (coordinator?.wasReplaced) workers?.detach(); | |
| const cleanup = await Promise.allSettled([ | |
| relay?.close(), | |
| backend?.close(), | |
| coordinator?.wasReplaced ? undefined : workers?.shutdown(), | |
| Promise.resolve(coordinator?.close()), | |
| released ? undefined : release(), | |
| ]); | |
| const cleanupErrors = cleanup.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])); | |
| if (cleanupErrors.length > 0) { | |
| throw new AggregateError([error, ...cleanupErrors], "Server runtime startup and cleanup failed"); | |
| } | |
| throw error; | |
| } | |
| } | |
| /** Start an operator-held server while serializing against automatic cold activation. */ | |
| export async function startForegroundServer( | |
| options: Omit<StartServerOptions, "keepAlive"> = {}, | |
| ): Promise<RunningServer> { | |
| const directory = resolveServerDirectory(options.directory); | |
| await ensurePrivateServerDirectory(directory); | |
| const profile = await acquireServerProfile(directory, options.serverId ?? process.env[ENV_SERVER_ID]); | |
| const serverId = profile.serverId; | |
| await profile.release(); | |
| const release = await acquireServerActivation(directory, serverId); | |
| try { | |
| return await startServer({ | |
| ...options, | |
| directory, | |
| serverId, | |
| keepAlive: true, | |
| }); | |
| } finally { | |
| await release(); | |
| } | |
| } | |
| function parseServerModelOptions(value: string | undefined): { provider?: string; model: string } | undefined { | |
| if (value === undefined) return undefined; | |
| let parsed: unknown; | |
| try { | |
| parsed = JSON.parse(value); | |
| } catch (error) { | |
| throw new Error("Internal server received invalid model options", { cause: error }); | |
| } | |
| if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { | |
| throw new Error("Internal server received invalid model options"); | |
| } | |
| const keys = Object.keys(parsed); | |
| const model = "model" in parsed ? parsed.model : undefined; | |
| const provider = "provider" in parsed ? parsed.provider : undefined; | |
| if ( | |
| keys.some((key) => key !== "provider" && key !== "model") || | |
| typeof model !== "string" || | |
| model.length === 0 || | |
| (provider !== undefined && (typeof provider !== "string" || provider.length === 0)) | |
| ) { | |
| throw new Error("Internal server received invalid model options"); | |
| } | |
| return provider === undefined ? { model } : { provider, model }; | |
| } | |
| /** Run an automatically activated server until its client and Session demand disappears. */ | |
| export async function runServerProcess(args: readonly string[]): Promise<void> { | |
| const [directory, serverId, sessionDir, serializedModel] = args; | |
| if (args.length > 4) throw new Error("Internal server received unexpected arguments"); | |
| if (!directory || !isAbsolute(directory)) throw new Error("Internal server requires an absolute server directory"); | |
| if (!isServerId(serverId)) throw new Error("Internal server requires a canonical server ID"); | |
| if (!sessionDir || !isAbsolute(sessionDir)) | |
| throw new Error("Internal server requires an absolute Session directory"); | |
| const workerModel = parseServerModelOptions(serializedModel); | |
| const runtime = await startServer({ | |
| directory, | |
| serverId, | |
| sessionDir, | |
| keepAlive: false, | |
| ...workerModel, | |
| }); | |
| const close = (): void => { | |
| void runtime.close().catch(() => {}); | |
| }; | |
| process.once("SIGINT", close); | |
| process.once("SIGTERM", close); | |
| try { | |
| await runtime.closed; | |
| } finally { | |
| process.off("SIGINT", close); | |
| process.off("SIGTERM", close); | |
| await runtime.close(); | |
| } | |
| } | |
| function sameStrings(left: readonly string[], right: readonly string[]): boolean { | |
| return left.length === right.length && left.every((value, index) => value === right[index]); | |
| } | |
| if (isDirectInternalProcessEntry(import.meta.url)) { | |
| const role = consumeInternalProcessRole(); | |
| if (role !== "server") throw new Error("Server entrypoint requires an internal server invocation"); | |
| void runServerProcess(process.argv.slice(2)).catch((error: unknown) => { | |
| console.error(error); | |
| process.exit(1); | |
| }); | |
| } | |