import { createLiveAcpSession, type LiveAcpSession, type ReverseRequestHandler } from "./acp-client.js"; import type { ReasoningEffort } from "./types.js"; export interface SessionState { session: LiveAcpSession; bootstrapped: boolean; expectedPrefixFingerprint?: string; } interface SessionKey { binary: string; modelId: string; reasoningEffort?: ReasoningEffort; cwd: string; } function serializeKey(key: SessionKey): string { return [key.binary, key.modelId, key.reasoningEffort ?? "", key.cwd].join("\0"); } export class SessionController { private state: SessionState | undefined; private key: string | undefined; private tail: Promise = Promise.resolve(); private disposed = false; private readonly reverseRequestHandler: ReverseRequestHandler; constructor(reverseRequestHandler: ReverseRequestHandler) { this.reverseRequestHandler = reverseRequestHandler; } run( params: SessionKey & { signal?: AbortSignal }, operation: (state: SessionState) => Promise, reuse?: (state: SessionState) => boolean, ): Promise { const run = this.tail.then(() => this.runNow(params, operation, reuse)); this.tail = run.then( () => undefined, () => undefined, ); return run; } restart(): Promise { const restart = this.tail.then(() => this.disposeState()); this.tail = restart.then( () => undefined, () => undefined, ); return restart; } dispose(): void { this.disposed = true; this.disposeState(); } private async runNow( params: SessionKey & { signal?: AbortSignal }, operation: (state: SessionState) => Promise, reuse?: (state: SessionState) => boolean, ): Promise { if (this.disposed) throw new Error("Grok Build session controller is disposed"); if (params.signal?.aborted) throw new Error("aborted"); const nextKey = serializeKey(params); if ( !this.state || this.key !== nextKey || this.state.session.process.exitCode !== null || this.state.session.process.signalCode !== null || (reuse !== undefined && !reuse(this.state)) ) { this.disposeState(); const session = await createLiveAcpSession({ binary: params.binary, modelId: params.modelId, ...(params.reasoningEffort ? { reasoningEffort: params.reasoningEffort } : {}), cwd: params.cwd, reverseRequestHandler: this.reverseRequestHandler, ...(params.signal ? { signal: params.signal } : {}), }); this.state = { session, bootstrapped: false }; this.key = nextKey; } return operation(this.state); } private disposeState(): void { this.state?.session.dispose(); this.state = undefined; this.key = undefined; } }