import { CodexSupervisor } from './classes.codexsupervisor.js'; import { CodexClientAdapter, type ICodexEventClock } from './classes.codexclientadapter.js'; import type { ControllerCodexConnectionStore } from './classes.codexconnectionstore.js'; import { assertCodexOrigin, codexOriginsEqual, type IControllerCodexOrigin, type IControllerCodexProfileDocument } from './classes.codexconnectionmodels.js'; import type { IControllerEvent, IControllerCodexProfile } from '../ts_interfaces/index.js'; import { codexQualifiedIdentity, isCodexThreadId } from './functions.codexidentity.js'; export interface ICodexConnectionRuntime { profile: IControllerCodexProfileDocument; supervisor: CodexSupervisor; client: CodexClientAdapter; } /** Routes opaque public conversations through durable origins, independently of current project mappings. */ export class CodexConnections { private readonly runtimes = new Map(); private readonly starting = new Map>(); private readonly origins = new Map(); private readonly clock: ICodexEventClock = { streamEpoch: Date.now(), revision: 0 }; private closed = false; constructor(private readonly options: { store: ControllerCodexConnectionStore; directory: string; executable?: string; defaultProfileId: string; onEvent: (eventArg: IControllerEvent) => void; onUnavailable: (runtimeArg: ICodexConnectionRuntime) => void; }) {} public bind(projectIdArg: string, nativeIdArg: string, originArg: IControllerCodexOrigin): void { assertCodexOrigin(originArg); const qualified = codexQualifiedIdentity(nativeIdArg); if (qualified ? qualified.profileId !== originArg.profileId || qualified.rawThreadId !== originArg.rawThreadId : nativeIdArg !== originArg.rawThreadId || !isCodexThreadId(nativeIdArg)) throw new Error('Codex public identity does not match its server origin.'); const previous = this.origins.get(nativeIdArg); if (previous && (previous.projectId !== projectIdArg || !codexOriginsEqual(previous.origin, originArg))) throw new Error('Codex conversation origin cannot change.'); if (!previous && this.origins.size >= 65536) throw new Error('Codex conversation origin capacity reached.'); this.origins.set(nativeIdArg, { projectId: projectIdArg, origin: structuredClone(originArg) }); } public origin(nativeIdArg: string, projectIdArg?: string): IControllerCodexOrigin { const binding = this.origins.get(nativeIdArg); if (!binding || (projectIdArg !== undefined && binding.projectId !== projectIdArg)) throw new Error('Codex conversation has no managed origin in this project.'); return structuredClone(binding.origin); } public runtime(profileIdArg = this.options.defaultProfileId): ICodexConnectionRuntime | undefined { return this.runtimes.get(profileIdArg); } public all(): ICodexConnectionRuntime[] { return [...this.runtimes.values()]; } public isCurrent(runtimeArg: ICodexConnectionRuntime): boolean { return this.runtimes.get(runtimeArg.profile.id) === runtimeArg && !this.closed; } public clientForSession(nativeIdArg: string): CodexClientAdapter { return this.require(this.origin(nativeIdArg).profileId).client; } public require(profileIdArg = this.options.defaultProfileId): ICodexConnectionRuntime { const runtime = this.runtimes.get(profileIdArg); if (!runtime || !runtime.supervisor.getStatus().healthy) throw new Error(runtime?.supervisor.getStatus().diagnostic?.message ?? 'This Codex connection is unavailable. Open Settings → Codex to connect it.'); return runtime; } public async profiles(): Promise { return (await this.options.store.list()).map(profile => ({ ...this.options.store.publicProfile(profile), ...(this.runtime(profile.id) ? { status: { harnessId: 'codex' as const, ...this.runtime(profile.id)!.supervisor.getStatus(), supportsLocalAttachments: this.runtime(profile.id)!.supervisor.supportsLocalAttachments } } : {}) })); } public connect(profileIdArg: string): Promise { if (this.closed) return Promise.reject(new Error('Codex connections are stopping.')); const pending = this.starting.get(profileIdArg); if (pending) return pending; if (this.runtimes.has(profileIdArg)) return Promise.resolve().then(() => this.require(profileIdArg)); const task = this.start(profileIdArg); this.starting.set(profileIdArg, task); void task.finally(() => { if (this.starting.get(profileIdArg) === task) this.starting.delete(profileIdArg); }).catch(() => undefined); return task; } private async start(profileIdArg: string): Promise { const profile = await this.options.store.get(profileIdArg); const token = await this.options.store.token(profile); if (this.closed) throw new Error('Codex connections are stopping.'); let runtime: ICodexConnectionRuntime; const supervisor = new CodexSupervisor({ directory: this.options.directory, executable: this.options.executable, ...(profile.serverUrl ? { serverUrl: profile.serverUrl, token } : {}), onNotification: notification => runtime?.client.onNotification(notification), onServerRequest: request => runtime?.client.onServerRequest(request), onUnavailable: () => { if (runtime && this.isCurrent(runtime)) this.options.onUnavailable(runtime); }, }); const legacyThreadIds = new Set([...this.origins].filter(([id, value]) => value.origin.profileId === profileIdArg && isCodexThreadId(id)).map(([id]) => id)); const client = new CodexClientAdapter(supervisor, event => { if (this.isCurrent(runtime) && !supervisor.signal.aborted) this.options.onEvent(event); }, this.clock, { profileId: profileIdArg, legacyThreadIds }); runtime = { profile, supervisor, client }; this.runtimes.set(profileIdArg, runtime); await supervisor.start(); return runtime; } public async disconnect(profileIdArg: string): Promise { await this.starting.get(profileIdArg)?.catch(() => undefined); const runtime = this.runtimes.get(profileIdArg); if (!runtime) return; await runtime.supervisor.stop(); if (this.runtimes.get(profileIdArg) === runtime) this.runtimes.delete(profileIdArg); } public async close(): Promise { this.closed = true; await Promise.allSettled([...this.starting.values()]); const results = await Promise.allSettled([...this.runtimes.values()].map(runtime => runtime.supervisor.stop())); const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []); if (errors.length) throw new AggregateError(errors, 'Codex connection cleanup is incomplete.'); this.runtimes.clear(); } }