import { randomUUID } from "node:crypto"; import { logger } from "../logger.js"; import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js"; import { removeRuntimeSessionContext, setRuntimeSessionContext, } from "../runtime/sessionContext.js"; import { toActorKey, toProjectKey } from "../utils/fileStore.js"; export type SessionBinding = { clientSessionId: string; sessionId: string; startedAt: number; }; export type SessionContext = { actorKey?: string; clientSessionId: string; network?: string; projectId?: string; userId?: string; projectKey?: string; }; export type ChatRequestContext = SessionContext & { actorKey: string; projectKey: string; traceId: string; }; export class ChatSessionBridge { private readonly abortControllers = new Map(); constructor(private readonly runtime: OpencodeRuntimeAdapter) {} async resolve(context: { sessionId?: string; network?: string; projectId?: string; traceId?: string; userId?: string; actorKey?: string; projectKey?: string; }): Promise<{ binding: SessionBinding; requestContext: ChatRequestContext; created: boolean; }> { let requestContext = this.buildRequestContext(context); const existingSessionId = context.sessionId?.trim(); await this.abortActiveRuntime(requestContext.clientSessionId, existingSessionId); let sessionId = existingSessionId; let created = false; if (!sessionId) { const session = await this.runtime.createSession(); sessionId = session.id; requestContext = { ...requestContext, clientSessionId: sessionId, }; created = true; } const binding: SessionBinding = { clientSessionId: requestContext.clientSessionId, sessionId, startedAt: Date.now(), }; const workspaceDirectory = await this.runtime.ensureSessionWorkspace?.(sessionId); setRuntimeSessionContext({ workspaceDirectory, actorKey: requestContext.actorKey, allowLearningWrite: true, clientSessionId: requestContext.clientSessionId, learningMode: "interactive", network: requestContext.network, projectId: requestContext.projectId, projectKey: requestContext.projectKey, sessionId, traceId: requestContext.traceId, }); return { binding, requestContext, created }; } count(): number { return this.abortControllers.size; } createClientSessionId() { return `agent-${randomUUID().slice(0, 12)}`; } registerAbortController(clientSessionId: string, controller: AbortController) { this.abortControllers.set(clientSessionId, controller); } finalizeRequest(clientSessionId: string) { this.abortControllers.delete(clientSessionId); } async abort(context: { clientSessionId?: string; sessionId?: string; }): Promise { const clientSessionId = context.clientSessionId?.trim(); const sessionId = context.sessionId?.trim(); if (!clientSessionId || !sessionId) { return null; } await this.abortActiveRuntime(clientSessionId, sessionId); return { clientSessionId, sessionId, startedAt: Date.now(), }; } async deleteSession(context: { clientSessionId: string; sessionId: string; }) { const clientSessionId = context.clientSessionId.trim(); const sessionId = context.sessionId.trim(); const controller = this.abortControllers.get(clientSessionId); if (controller) { this.abortControllers.delete(clientSessionId); controller.abort(); } await this.runtime.abortSession(sessionId).catch((error) => { logger.warn( { clientSessionId, sessionId, err: error }, "failed to abort runtime session", ); }); await this.runtime.waitForSessionIdle(sessionId).catch((error) => { logger.warn( { clientSessionId, sessionId, err: error }, "failed while waiting for runtime session to become idle", ); }); removeRuntimeSessionContext(sessionId); } private buildRequestContext(context: { actorKey?: string; sessionId?: string; network?: string; projectId?: string; traceId?: string; userId?: string; projectKey?: string; }): ChatRequestContext { const sessionId = context.sessionId?.trim(); return { clientSessionId: sessionId || this.createClientSessionId(), actorKey: context.actorKey ?? toActorKey(`local:${context.userId ?? "local-user"}`), network: context.network, projectId: context.projectId, projectKey: context.projectKey ?? toProjectKey(`local:${context.projectId ?? "local-project"}`), traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`, userId: context.userId?.trim(), }; } private async abortActiveRuntime(clientSessionId: string, sessionId?: string) { const controller = this.abortControllers.get(clientSessionId); if (controller) { this.abortControllers.delete(clientSessionId); controller.abort(); } if (!sessionId) { return; } await this.runtime.abortSession(sessionId).catch((error) => { logger.warn( { clientSessionId, sessionId, err: error }, "failed to abort active runtime session", ); }); await this.runtime.waitForSessionIdle(sessionId).catch((error) => { logger.warn( { clientSessionId, sessionId, err: error }, "failed while waiting for active runtime session to become idle", ); }); } }