import { createCodingConversationStore, type CodingConversationV2, type PiSessionBinding, } from '../../coding-projects/conversation-store'; import { readCodingProjectConfigV2, type CodingProjectAgentV2, } from '../../coding-projects/project-config'; import type { CodingProjectStore } from '../../coding-projects/project-store'; import type { ConversationModelState, PrepareConversationInput, } from '../contracts'; import { CodingRuntimeContractError } from '../runtime-errors'; export interface PiRegisteredConversation { projectPath: string; conversation: CodingConversationV2; agent: CodingProjectAgentV2; session: PiSessionBinding | null; } export interface PiSessionRegistryOptions { projectStore: CodingProjectStore; createConversationStore?: typeof createCodingConversationStore; } interface RegistryRecord extends PiRegisteredConversation { store: ReturnType; } function publicRecord(record: RegistryRecord): PiRegisteredConversation { return { projectPath: record.projectPath, conversation: structuredClone(record.conversation), agent: structuredClone(record.agent), session: record.session ? structuredClone(record.session) : null, }; } function modelStateOf(conversation: CodingConversationV2): ConversationModelState { return { model: conversation.model ? structuredClone(conversation.model) : null, modelResolution: conversation.modelResolution, }; } function sameModelState(left: ConversationModelState, right: ConversationModelState): boolean { return left.modelResolution === right.modelResolution && (left.model === null || right.model === null ? left.model === right.model : left.model.accountId === right.model.accountId && left.model.modelId === right.model.modelId && left.model.thinkingLevel === right.model.thinkingLevel && JSON.stringify(left.model.reasoningChoice) === JSON.stringify(right.model.reasoningChoice)); } export class PiSessionRegistry { private readonly projectStore: CodingProjectStore; private readonly createConversationStore: typeof createCodingConversationStore; private readonly records = new Map(); private readonly prepareFlights = new Map>(); private readonly stores = new Map>(); constructor(options: PiSessionRegistryOptions) { this.projectStore = options.projectStore; this.createConversationStore = options.createConversationStore ?? createCodingConversationStore; } async prepare(input: PrepareConversationInput): Promise { return publicRecord(await this.prepareRecord(input)); } async ensureBinding( input: PrepareConversationInput, createBinding: () => Promise, ): Promise { const record = await this.prepareRecord(input); const conversation = await this.persistWrite( () => record.store.ensureSessionBinding(input.conversationId, createBinding), ); record.conversation = conversation; record.session = { piSessionId: conversation.piSessionId as string, sessionKey: conversation.sessionKey as string, }; return publicRecord(record); } async setModel( conversationId: string, model: ConversationModelState, ): Promise { const record = this.records.get(conversationId); if (!record) throw new Error('Conversation is not registered'); record.conversation = await this.persistWrite( () => record.store.setModelState(conversationId, model), ); return modelStateOf(record.conversation); } forget(conversationId: string): void { this.records.delete(conversationId); } private prepareRecord(input: PrepareConversationInput): Promise { const existing = this.records.get(input.conversationId); if (existing) return Promise.resolve(existing); const pending = this.prepareFlights.get(input.conversationId); if (pending) return pending; const flight = this.load(input).finally(() => { if (this.prepareFlights.get(input.conversationId) === flight) { this.prepareFlights.delete(input.conversationId); } }); this.prepareFlights.set(input.conversationId, flight); return flight; } private async load(input: PrepareConversationInput): Promise { const project = (await this.projectStore.listProjects()) .find((candidate) => candidate.id === input.projectId); if (!project) throw new Error('Coding project does not exist'); const configRead = await readCodingProjectConfigV2(project.path); if (configRead.status !== 'valid') throw new Error('Coding project configuration is unavailable'); const agent = configRead.config.agents.find((candidate) => ( candidate.id === input.agentId && candidate.enabled && !candidate.archivedAt )); if (!agent) throw new Error('Coding Agent does not exist'); let store = this.stores.get(project.path); if (!store) { store = this.createConversationStore(project.path); this.stores.set(project.path, store); } const conversation = await store.get(input.conversationId); if (!conversation || conversation.agentId !== input.agentId) { throw new Error('Coding Conversation does not exist for the selected Agent'); } if (!sameModelState(modelStateOf(conversation), input.model)) { throw new Error('Coding Conversation model metadata is stale'); } const record: RegistryRecord = { projectPath: project.path, conversation, agent, session: conversation.piSessionId && conversation.sessionKey ? { piSessionId: conversation.piSessionId, sessionKey: conversation.sessionKey } : null, store, }; this.records.set(input.conversationId, record); return record; } private async persistWrite(operation: () => Promise): Promise { try { return await operation(); } catch (error) { if (error instanceof CodingRuntimeContractError) throw error; throw new CodingRuntimeContractError( 'CODING_STORAGE_WRITE_FAILED', 'Coding Conversation state could not be persisted', true, ); } } }