// Prisma schema for the personal chat DB + LLM call log generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } enum Provider { openai anthropic xai gemini hermes_agent @map("hermes-agent") } enum MessageRole { system user assistant tool } enum SearchSource { exa } enum ProjectKind { starred folder } model User { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // minimal for now (single-user is fine). Keep extensible. handle String? @unique chats Chat[] searches Search[] projects Project[] } model Chat { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt title String? titleGenerationPending Boolean @default(false) initiatedProvider Provider? initiatedModel String? lastUsedProvider Provider? lastUsedModel String? additionalSystemPrompt String? enabledTools Json? user User? @relation(fields: [userId], references: [id]) userId String? // Forks always point directly to the single root chat, never to another fork. parentChat Chat? @relation("ChatForks", fields: [parentChatId], references: [id], onDelete: Cascade) parentChatId String? childChats Chat[] @relation("ChatForks") messages Message[] calls LlmCall[] projectItems ProjectItem[] @@index([userId]) @@index([parentChatId]) @@index([updatedAt]) } model Message { id String @id @default(cuid()) createdAt DateTime @default(now()) chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade) chatId String role MessageRole content String // for tool messages or attachments later name String? metadata Json? @@index([chatId, createdAt]) } model LlmCall { id String @id @default(cuid()) createdAt DateTime @default(now()) chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade) chatId String provider Provider model String // request/response snapshots for debugging + replay request Json response Json? // usage/cost basics inputTokens Int? outputTokens Int? totalTokens Int? latencyMs Int? error String? @@index([chatId, createdAt]) @@index([provider, model, createdAt]) } model Search { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt title String? query String? queryNormalized String? source SearchSource @default(exa) requestId String? rawResponse Json? latencyMs Int? error String? answerText String? answerRequestId String? answerCitations Json? answerRawResponse Json? answerError String? user User? @relation(fields: [userId], references: [id]) userId String? results SearchResult[] projectItems ProjectItem[] @@index([updatedAt]) @@index([queryNormalized, updatedAt]) @@index([userId]) } model SearchResult { id String @id @default(cuid()) createdAt DateTime @default(now()) search Search @relation(fields: [searchId], references: [id], onDelete: Cascade) searchId String rank Int title String? url String publishedDate String? author String? text String? highlights Json? highlightScores Json? score Float? favicon String? image String? @@index([searchId, rank]) } model Project { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt kind ProjectKind @default(folder) title String user User? @relation(fields: [userId], references: [id], onDelete: Cascade) userId String? items ProjectItem[] @@index([kind]) @@index([userId]) } model ProjectItem { id String @id @default(cuid()) createdAt DateTime @default(now()) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) projectId String chat Chat? @relation(fields: [chatId], references: [id], onDelete: Cascade) chatId String? search Search? @relation(fields: [searchId], references: [id], onDelete: Cascade) searchId String? @@unique([projectId, chatId]) @@unique([projectId, searchId]) @@index([projectId, createdAt]) @@index([chatId]) @@index([searchId]) } // Shared personal read position for the external persistent Sybil Agent room. // Messages remain in the agent's durable transcript, not duplicated in Sybil Chat. model AgentReadState { sourceId String? id String @id cursor String @default("0") updatedAt DateTime @updatedAt } // One server-derived shared principal. Transport payloads are secret DB data. model AgentPushInstallation { id String @id ownerEpoch String transport String payload String fingerprint String enabled Boolean @default(false) version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lastClearAt DateTime? revokedAt DateTime? @@index([ownerEpoch, enabled]) @@index([fingerprint]) } model AgentPushPresence { id String @id ownerEpoch String sequence Int visible Boolean focused Boolean lastInteractionAt DateTime? inactiveUntil DateTime? expiresAt DateTime @@index([ownerEpoch, expiresAt]) } model AgentPushConnection { id String @id ownerEpoch String kind String sessionId String? expiresAt DateTime @@index([ownerEpoch, expiresAt]) } model AgentPushCoordinator { id String @id ownerEpoch String sourceId String? cursor String @default("0") quarantined Boolean @default(false) contextObservedAt DateTime? latestUserMessageAt DateTime? localPromptAt DateTime? nativeActive Boolean @default(false) nativeUnknown Boolean @default(true) unknownSince DateTime? uncertain Boolean @default(true) lockOwner String? lockUntil DateTime? } model AgentPushDelivery { id String @id ownerEpoch String installationId String installationVersion Int sourceId String messageId String cursor String kind String trigger String priority Int @default(0) status String @default("pending") attempts Int @default(0) createdAt DateTime @default(now()) dueAt DateTime expiresAt DateTime updatedAt DateTime @updatedAt outcome String? lastSuppressionReason String? lastSuppressionClientKind String? lastSuppressedAt DateTime? @@index([status, dueAt]) @@index([installationId, status]) @@index([ownerEpoch, sourceId, cursor]) }