import { nanoid } from "nanoid"; import type { AssistantConversationItem, AssistantSessionState } from "../types/assistant"; import type { InvestigationState } from "../types/stage1Contracts"; import { FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 } from "../config"; import { cloneInvestigationState, createEmptyInvestigationState } from "./investigationState"; const MAX_ITEMS_PER_SESSION = 200; function cloneItem(item: AssistantConversationItem): AssistantConversationItem { return { ...item, debug: item.debug ? { ...item.debug } : null }; } function cloneSession(state: AssistantSessionState): AssistantSessionState { return { session_id: state.session_id, updated_at: state.updated_at, items: state.items.map(cloneItem), investigation_state: cloneInvestigationState(state.investigation_state) }; } function normalizeSessionShape(state: AssistantSessionState): AssistantSessionState { const legacy = state as AssistantSessionState & { investigation_state?: InvestigationState | null; items?: AssistantConversationItem[]; updated_at?: string; }; const normalizedItems = Array.isArray(legacy.items) ? legacy.items : []; const investigationState = FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 ? legacy.investigation_state ?? createEmptyInvestigationState(state.session_id) : legacy.investigation_state ?? null; state.items = normalizedItems; state.updated_at = typeof legacy.updated_at === "string" && legacy.updated_at.trim() ? legacy.updated_at : new Date().toISOString(); state.investigation_state = investigationState; return state; } export class AssistantSessionStore { private readonly sessions = new Map(); public ensureSession(sessionId?: string): AssistantSessionState { const resolvedId = (sessionId ?? "").trim() || `asst-${nanoid(10)}`; const existing = this.sessions.get(resolvedId); if (existing) { return cloneSession(normalizeSessionShape(existing)); } const created: AssistantSessionState = { session_id: resolvedId, updated_at: new Date().toISOString(), items: [], investigation_state: FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 ? createEmptyInvestigationState(resolvedId) : null }; this.sessions.set(resolvedId, created); return cloneSession(created); } public appendItem(sessionId: string, item: AssistantConversationItem): AssistantConversationItem { const session = this.ensureMutableSession(sessionId); session.items.push(item); if (session.items.length > MAX_ITEMS_PER_SESSION) { session.items = session.items.slice(session.items.length - MAX_ITEMS_PER_SESSION); } session.updated_at = new Date().toISOString(); return cloneItem(item); } public getSession(sessionId: string): AssistantSessionState | null { const found = this.sessions.get(sessionId); return found ? cloneSession(normalizeSessionShape(found)) : null; } public setInvestigationState(sessionId: string, state: InvestigationState | null): InvestigationState | null { const session = this.ensureMutableSession(sessionId); session.investigation_state = cloneInvestigationState(state); session.updated_at = new Date().toISOString(); return cloneInvestigationState(session.investigation_state); } private ensureMutableSession(sessionId: string): AssistantSessionState { const existing = this.sessions.get(sessionId); if (existing) { return normalizeSessionShape(existing); } const created: AssistantSessionState = { session_id: sessionId, updated_at: new Date().toISOString(), items: [], investigation_state: FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 ? createEmptyInvestigationState(sessionId) : null }; this.sessions.set(sessionId, created); return created; } }