seroxdesigncleand0badd58 months agoHistory
638 lines · 20.5 KB · TypeScript
RawDownload
// CRITICALimport { randomUUID } from "node:crypto";import { hydrateMessageRow, hydrateSessionRow } from "./store-hydration";import { migrateChatStore } from "./store-schema";import * as RunOps from "./store-runs";import { openSqliteDatabase } from "../../stores/sqlite";import type {  ChatAgentFileVersion,  ChatAgentFileVersionWrite,  ChatMessage,  ChatRun,  ChatRunEvent,  ChatSession,  ChatSessionListItem,  ChatSessionSummary,  ChatToolExecution,  ChatUsage,} from "../../types/chat"; /** * SQLite-backed chat session storage. */export class ChatStore {  private readonly db: ReturnType<typeof openSqliteDatabase>;   /**   * Create a chat store.   * @param dbPath - SQLite database path.   */  public constructor(dbPath: string) {    this.db = openSqliteDatabase(dbPath);    this.migrate();  }   /**   * Initialize schema.   * @returns void   */  private migrate(): void {    migrateChatStore(this.db);  }   /**   * List all chat sessions.   * @returns List of sessions.   */  public listSessions(): Array<ChatSessionListItem> {    const rows = this.db      .query(        "SELECT id, title, model, parent_id, created_at, updated_at FROM chat_sessions ORDER BY updated_at DESC"      )      .all() as Array<Record<string, unknown>>;    return rows.map((row) => ({ ...row }) as ChatSessionListItem);  }   /**   * Get a session summary without messages.   * @param sessionId - Session identifier.   * @returns Session summary or null.   */  public getSessionSummary(sessionId: string): ChatSessionSummary | null {    const session = hydrateSessionRow(      this.db        .query(          "SELECT id, title, model, parent_id, agent_state, created_at, updated_at FROM chat_sessions WHERE id = ?"        )        .get(sessionId) as Record<string, unknown> | null    );    return session ? ({ ...session } as ChatSessionSummary) : null;  }   /**   * Get a session and its messages.   * @param sessionId - Session identifier.   * @returns Session object or null.   */  public getSession(sessionId: string): ChatSession | null {    const rawSession = this.db      .query(        "SELECT id, title, model, parent_id, agent_state, created_at, updated_at FROM chat_sessions WHERE id = ?"      )      .get(sessionId) as Record<string, unknown> | null;    const session = hydrateSessionRow(rawSession);    if (!session) {      return null;    }     const messages = this.db      .query(        `SELECT id, role, content, model, tool_calls, tool_call_id, name, parts, metadata,              request_prompt_tokens, request_tools_tokens, request_total_input_tokens, request_completion_tokens,              created_at              FROM chat_messages WHERE session_id = ? ORDER BY created_at, rowid`      )      .all(sessionId) as Array<Record<string, unknown>>;     const hydrated = messages.map((message) => hydrateMessageRow(message));     return { ...session, messages: hydrated } as ChatSession;  }   /**   * Create a new chat session.   * @param sessionId - Session identifier.   * @param title - Session title.   * @param model - Model name.   * @param parentId - Parent session id.   * @param agentState   * @returns Created session.   */  public createSession(    sessionId: string,    title = "New Chat",    model?: string,    parentId?: string,    agentState?: unknown  ): ChatSessionSummary {    const agentStateJson =      agentState !== undefined && agentState !== null ? JSON.stringify(agentState) : null;    this.db      .query(        "INSERT INTO chat_sessions (id, title, model, parent_id, agent_state) VALUES (?, ?, ?, ?, ?)"      )      .run(sessionId, title, model ?? null, parentId ?? null, agentStateJson);    const row = this.db      .query(        "SELECT id, title, model, parent_id, agent_state, created_at, updated_at FROM chat_sessions WHERE id = ?"      )      .get(sessionId) as Record<string, unknown>;    const hydrated = hydrateSessionRow(row);    return { ...(hydrated ?? row) } as ChatSessionSummary;  }   /**   * Update session title or model.   * @param sessionId - Session identifier.   * @param title - New title.   * @param model - New model.   * @param agentState   * @returns True if updated.   */  public updateSession(    sessionId: string,    title?: string,    model?: string,    agentState?: unknown  ): boolean {    const updates: string[] = [];    const params: Array<string | null> = [];    if (title !== undefined) {      updates.push("title = ?");      params.push(title);    }    if (model !== undefined) {      updates.push("model = ?");      params.push(model);    }    if (agentState !== undefined) {      updates.push("agent_state = ?");      params.push(agentState === null ? null : JSON.stringify(agentState));    }    if (updates.length === 0) {      return true;    }    updates.push("updated_at = CURRENT_TIMESTAMP");    params.push(sessionId);    const result = this.db      .query(`UPDATE chat_sessions SET ${updates.join(", ")} WHERE id = ?`)      .run(...params);    return result.changes > 0;  }   /**   * Delete a chat session and its messages.   * @param sessionId - Session identifier.   * @returns True if deleted.   */  public deleteSession(sessionId: string): boolean {    this.db.query("DELETE FROM chat_messages WHERE session_id = ?").run(sessionId);    const result = this.db.query("DELETE FROM chat_sessions WHERE id = ?").run(sessionId);    return result.changes > 0;  }   /**   * Add a message to a session.   * @param sessionId - Session identifier.   * @param messageId - Message identifier.   * @param role - Message role.   * @param content - Message content.   * @param model - Model name.   * @param toolCalls - Tool call array.   * @param promptTokens - Prompt token count.   * @param toolsTokens - Tools token count.   * @param totalInputTokens - Total input tokens.   * @param completionTokens - Completion tokens.   * @param parts - Structured message parts.   * @param metadata - Message metadata.   * @param toolCallId - Tool call identifier.   * @param name - Tool name (for tool messages).   * @returns Stored message.   */  public addMessage(    sessionId: string,    messageId: string,    role: string,    content?: string,    model?: string,    toolCalls?: unknown[],    promptTokens?: number,    toolsTokens?: number,    totalInputTokens?: number,    completionTokens?: number,    parts?: unknown[],    metadata?: unknown,    toolCallId?: string,    name?: string  ): ChatMessage {    const toolCallsJson = toolCalls ? JSON.stringify(toolCalls) : null;    const partsJson = parts ? JSON.stringify(parts) : null;    const metadataJson =      metadata !== undefined && metadata !== null ? JSON.stringify(metadata) : null;    this.db      .query(        `INSERT INTO chat_messages      (id, session_id, role, content, model, tool_calls, tool_call_id, name, parts, metadata,       request_prompt_tokens, request_tools_tokens, request_total_input_tokens, request_completion_tokens)      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)      ON CONFLICT(id) DO UPDATE SET        role = excluded.role,        model = excluded.model,        content = excluded.content,        tool_calls = excluded.tool_calls,        tool_call_id = excluded.tool_call_id,        name = excluded.name,        parts = excluded.parts,        metadata = excluded.metadata,        request_prompt_tokens = excluded.request_prompt_tokens,        request_tools_tokens = excluded.request_tools_tokens,        request_total_input_tokens = excluded.request_total_input_tokens,        request_completion_tokens = excluded.request_completion_tokens`      )      .run(        messageId,        sessionId,        role,        content ?? null,        model ?? null,        toolCallsJson,        toolCallId ?? null,        name ?? null,        partsJson,        metadataJson,        promptTokens ?? null,        toolsTokens ?? null,        totalInputTokens ?? null,        completionTokens ?? null      );    this.db      .query("UPDATE chat_sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ?")      .run(sessionId);    const row = this.db      .query(        `SELECT id, role, content, model, tool_calls, tool_call_id, name, parts, metadata,         request_prompt_tokens, request_tools_tokens, request_total_input_tokens, request_completion_tokens, created_at         FROM chat_messages WHERE id = ?`      )      .get(messageId) as Record<string, unknown>;    return hydrateMessageRow(row);  }   /**   * Get usage totals for a session.   * @param sessionId - Session identifier.   * @returns Usage stats.   */  public getUsage(sessionId: string): ChatUsage {    const row = this.db      .query(        `SELECT          SUM(            CASE              WHEN COALESCE(request_total_input_tokens, 0) > 0 THEN COALESCE(request_total_input_tokens, 0)              ELSE COALESCE(request_prompt_tokens, 0)            END          ) AS prompt_tokens,          SUM(COALESCE(request_completion_tokens, 0)) AS completion_tokens         FROM chat_messages         WHERE session_id = ?`      )      .get(sessionId) as {      prompt_tokens?: number | null;      completion_tokens?: number | null;    } | null;     const prompt = Number(row?.prompt_tokens ?? 0);    const completion = Number(row?.completion_tokens ?? 0);    return {      prompt_tokens: prompt,      completion_tokens: completion,      total_tokens: prompt + completion,    };  }   /**   * Fork an existing session.   * @param sessionId - Original session id.   * @param newId - New session id.   * @param messageId - Optional message id to stop copying.   * @param model - New model.   * @param title - New title.   * @returns Forked session or null.   */  public forkSession(    sessionId: string,    newId: string,    messageId?: string,    model?: string,    title?: string  ): ChatSessionSummary | null {    const toNullableString = (value: unknown): string | null => {      if (value === null || value === undefined) {        return null;      }      return String(value);    };     const toNullableNumber = (value: unknown): number | null => {      if (value === null || value === undefined) {        return null;      }      const parsed = Number(value);      return Number.isNaN(parsed) ? null : parsed;    };     const original = this.getSession(sessionId);    if (!original) {      return null;    }    const newTitle = title ?? `${String(original["title"])} (fork)`;    const newModel = model ?? (original["model"] ? String(original["model"]) : undefined);    const agentState = original["agent_state"] ?? null;    const agentStateJson = agentState !== null ? JSON.stringify(agentState) : null;     this.db      .query(        "INSERT INTO chat_sessions (id, title, model, parent_id, agent_state) VALUES (?, ?, ?, ?, ?)"      )      .run(newId, newTitle, newModel ?? null, sessionId, agentStateJson);     const messages = (original["messages"] ?? []) as Array<Record<string, unknown>>;    const insertMessage = this.db.query(      `INSERT INTO chat_messages       (id, session_id, role, content, model, tool_calls, tool_call_id, name, parts, metadata,        request_prompt_tokens, request_tools_tokens, request_total_input_tokens, request_completion_tokens)       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`    );     const tx = this.db.transaction(() => {      for (const message of messages) {        const toolCallsJson = message["tool_calls"] ? JSON.stringify(message["tool_calls"]) : null;        const partsJson = message["parts"] ? JSON.stringify(message["parts"]) : null;        const metadataJson = message["metadata"] ? JSON.stringify(message["metadata"]) : null;        const newMessageId = `${newId}_${String(message["id"])}`;        const role = String(message["role"] ?? "");        const content = toNullableString(message["content"]);        const messageModel = toNullableString(message["model"]);        const toolCallId = toNullableString(message["tool_call_id"]);        const toolName = toNullableString(message["name"]);        const promptTokens = toNullableNumber(message["request_prompt_tokens"]);        const toolTokens = toNullableNumber(message["request_tools_tokens"]);        const totalTokens = toNullableNumber(message["request_total_input_tokens"]);        const completionTokens = toNullableNumber(message["request_completion_tokens"]);         insertMessage.run(          newMessageId,          newId,          role,          content,          messageModel,          toolCallsJson,          toolCallId,          toolName,          partsJson,          metadataJson,          promptTokens,          toolTokens,          totalTokens,          completionTokens        );         if (messageId && message["id"] === messageId) {          break;        }      }    });     tx();     const row = this.db      .query(        "SELECT id, title, model, parent_id, agent_state, created_at, updated_at FROM chat_sessions WHERE id = ?"      )      .get(newId) as Record<string, unknown>;    const hydrated = hydrateSessionRow(row);    return { ...(hydrated ?? row) } as ChatSessionSummary;  }   /**   * Create a run record.   * @param runId - Run identifier.   * @param sessionId - Session identifier.   * @param options - Optional run fields.   * @param options.userMessageId - User message id.   * @param options.model - Model name.   * @param options.system - System prompt.   * @param options.toolsetId - Toolset identifier.   * @param options.status - Run status.   * @returns Run record.   */  public createRun(    runId: string,    sessionId: string,    options: {      userMessageId?: string;      model?: string;      system?: string;      toolsetId?: string;      status?: string;    } = {}  ): ChatRun {    return RunOps.createRun(this.db, runId, sessionId, options);  }   /**   * Add a run event.   * @param runId - Run identifier.   * @param seq - Sequence number.   * @param type - Event type.   * @param data - Event payload.   * @param eventId - Optional event id override.   * @returns Stored event record.   */  public addRunEvent(    runId: string,    seq: number,    type: string,    data: Record<string, unknown>,    eventId: string = randomUUID()  ): ChatRunEvent {    return RunOps.addRunEvent(this.db, runId, seq, type, data, eventId);  }   /**   * Add a tool execution record.   * @param runId - Run identifier.   * @param toolCallId - Tool call id.   * @param toolName - Tool name (canonical server__tool).   * @param options - Optional fields.   * @param options.toolServer - MCP server id.   * @param options.arguments - Tool arguments payload.   * @param options.resultText - Tool result text.   * @param options.isError - Whether the tool errored.   * @param options.startedAt - Execution start timestamp.   * @param options.finishedAt - Execution finish timestamp.   * @param options.id - Override record id.   * @returns Stored tool execution record.   */  public addToolExecution(    runId: string,    toolCallId: string,    toolName: string,    options: {      toolServer?: string;      arguments?: Record<string, unknown> | string;      resultText?: string | null;      isError?: boolean;      startedAt?: string;      finishedAt?: string;      id?: string;    } = {}  ): ChatToolExecution {    return RunOps.addToolExecution(this.db, runId, toolCallId, toolName, options);  }   /**   * Update a run record.   * @param runId - Run identifier.   * @param updates - Fields to update.   * @param updates.status - Run status.   * @param updates.finishedAt - Finish timestamp.   * @returns True if updated.   */  public updateRun(    runId: string,    updates: {      status?: string;      finishedAt?: string | null;    }  ): boolean {    return RunOps.updateRun(this.db, runId, updates);

Lines 1–500 of 638.