// src/services/chat.service.ts // Copyright (C) 2026 DTP Technologies, LLC // All Rights Reserved import { Types } from "mongoose"; import { ChatSession, ChatSessionType, ChatSessionMode, } from "../models/chat-session.js"; import { ChatHistory, IChatHistory as IChatHistoryModel, ChatHistoryStatus, } from "../models/chat-history.js"; import { DtpService } from "../lib/service.js"; export interface IChatSessionListItem { _id: string; name: string; lastMessageAt: Date | null; turnCount: number; toolCallCount: number; inputTokens: number; outputTokens: number; createdAt: Date; mode: ChatSessionMode; } export interface IChatSessionDetail extends IChatSessionListItem { user: string; project?: string; type: ChatSessionType; pins: Array<{ _id?: string; content: string }>; } export interface IChatHistoryEntry { _id: string; sessionId: string; prompt: string; response: { thinking?: string; message?: string; }; status: ChatHistoryStatus; toolCalls: Array<{ tool: { name: string; callId: string; parameters: Array<{ name: string; value: string }>; }; response?: string; fileOperation?: { type: "read" | "write" | "edit" | "shell"; path?: string; diff?: string; linesAdded?: number; linesRemoved?: number; isBinary?: boolean; }; subagentStats?: { inputTokens: number; outputTokens: number; toolCallCount: number; }; }>; fileOperations: Array<{ type: "read" | "write" | "edit" | "shell"; path?: string; diff?: string; linesAdded?: number; linesRemoved?: number; isBinary?: boolean; }>; inputTokens: number; outputTokens: number; createdAt: Date; mode: ChatSessionMode; isSubagent: boolean; error?: { message: string; stack?: string; timestamp: Date; }; } export class ChatService extends DtpService { get name(): string { return "ChatService"; } get slug(): string { return "chat"; } async start(): Promise { this.log.info("service started"); } async stop(): Promise { this.log.info("service stopped"); } // ==================== Session Management ==================== async listByProject( projectId: string, userId: string, ): Promise { const sessions = await ChatSession.find({ user: userId, project: projectId, }) .sort({ lastMessageAt: -1, createdAt: -1 }) .lean(); return sessions.map((s) => ({ _id: s._id.toString(), name: s.name, lastMessageAt: s.lastMessageAt ?? null, turnCount: s.stats.turnCount, toolCallCount: s.stats.toolCallCount, inputTokens: s.stats.inputTokens, outputTokens: s.stats.outputTokens, createdAt: s.createdAt, mode: s.mode, })); } async listAll(userId: string): Promise { const sessions = await ChatSession.find({ user: userId, }) .sort({ lastMessageAt: -1, createdAt: -1 }) .limit(50) .lean(); return sessions.map((s) => ({ _id: s._id.toString(), name: s.name, lastMessageAt: s.lastMessageAt ?? null, turnCount: s.stats.turnCount, toolCallCount: s.stats.toolCallCount, inputTokens: s.stats.inputTokens, outputTokens: s.stats.outputTokens, createdAt: s.createdAt, mode: s.mode, })); } async create( userId: string, projectId?: string, name?: string, ): Promise { const session = new ChatSession({ user: userId, project: projectId, name: name ?? "New Session", type: ChatSessionType.Desktop, mode: ChatSessionMode.Build, }); await session.save(); this.log.info("ChatSession created", { sessionId: session._id, userId, projectId, }); return { _id: session._id.toString(), name: session.name, lastMessageAt: null, turnCount: 0, toolCallCount: 0, inputTokens: 0, outputTokens: 0, createdAt: session.createdAt, mode: session.mode, }; } async findById(sessionId: string): Promise { const session = await ChatSession.findById(sessionId).lean(); if (!session) { return null; } const sessionWithProject = await ChatSession.findById(sessionId) .populate("project", "name") .lean() as any; const projectName = (sessionWithProject?.project as any)?.name; const userId = session.user instanceof Types.ObjectId ? session.user.toString() : String(session.user); return { _id: session._id.toString(), name: session.name, lastMessageAt: session.lastMessageAt ?? null, turnCount: session.stats.turnCount, toolCallCount: session.stats.toolCallCount, inputTokens: session.stats.inputTokens, outputTokens: session.stats.outputTokens, createdAt: session.createdAt, mode: session.mode, user: userId, project: projectName, type: session.type, pins: session.pins.map((p) => ({ _id: p._id?.toString(), content: p.content, })), }; } async updateName(sessionId: string, name: string): Promise { await ChatSession.findByIdAndUpdate(sessionId, { name }); this.log.info("ChatSession name updated", { sessionId, name }); } async updateMode(sessionId: string, mode: ChatSessionMode): Promise { await ChatSession.findByIdAndUpdate(sessionId, { mode }); this.log.info("ChatSession mode updated", { sessionId, mode }); } async delete(sessionId: string, userId: string): Promise { const result = await ChatSession.deleteOne({ _id: sessionId, user: userId, }); if (result.deletedCount === 0) { throw new Error("ChatSession not found or not owned by user"); } this.log.info("ChatSession deleted", { sessionId, userId }); } // ==================== History Management ==================== async getHistory(sessionId: string): Promise { const history = await ChatHistory.find({ session: sessionId }) .sort({ createdAt: 1 }) .lean(); return history.map((h) => ({ _id: h._id.toString(), sessionId: h.session.toString(), prompt: h.prompt, response: { thinking: h.response?.thinking, message: h.response?.message, }, status: h.status, toolCalls: h.toolCalls.map((tc) => ({ tool: { name: tc.tool.name, callId: tc.tool.callId, parameters: tc.tool.parameters.map((p) => ({ name: p.name, value: p.value, })), }, response: tc.response, fileOperation: tc.fileOperation, subagentStats: tc.subagentStats, })), fileOperations: h.fileOperations, inputTokens: h.inputTokens, outputTokens: h.outputTokens, createdAt: h.createdAt, mode: h.mode, isSubagent: h.isSubagent ?? false, subagentHistory: h.subagentHistory || [], error: h.error, })); } async createHistoryEntry( sessionId: string, userId: string, prompt: string, mode: ChatSessionMode, ): Promise { const entry = new ChatHistory({ session: sessionId, user: userId, prompt, mode, status: ChatHistoryStatus.Processing, }); await entry.save(); // Update session stats await ChatSession.findByIdAndUpdate(sessionId, { $inc: { "stats.turnCount": 1, "stats.inputTokens": entry.inputTokens, "stats.outputTokens": entry.outputTokens, }, lastMessageAt: new Date(), }); this.log.info("ChatHistory entry created", { historyId: entry._id, sessionId, userId, }); return { _id: entry._id.toString(), sessionId, prompt: entry.prompt, response: { thinking: entry.response?.thinking, message: entry.response?.message, }, status: entry.status, toolCalls: [], fileOperations: [], inputTokens: 0, outputTokens: 0, createdAt: entry.createdAt, mode: entry.mode, isSubagent: entry.isSubagent ?? false, }; } async updateHistoryEntry( historyId: string, updates: Partial<{ response: { thinking?: string; message?: string }; status: ChatHistoryStatus; toolCalls: IChatHistoryModel["toolCalls"]; fileOperations: IChatHistoryModel["fileOperations"]; inputTokens: number; outputTokens: number; error: { message: string; stack?: string; timestamp: Date }; }>, ): Promise { const updateData: any = {}; if (updates.response !== undefined) { updateData.response = updates.response; } if (updates.status !== undefined) { updateData.status = updates.status; } if (updates.toolCalls !== undefined) { updateData.toolCalls = updates.toolCalls; } if (updates.fileOperations !== undefined) { updateData.fileOperations = updates.fileOperations; } if (updates.inputTokens !== undefined) { updateData.inputTokens = updates.inputTokens; } if (updates.outputTokens !== undefined) { updateData.outputTokens = updates.outputTokens; } if (updates.error !== undefined) { updateData.error = updates.error; } await ChatHistory.findByIdAndUpdate(historyId, updateData); this.log.info("ChatHistory entry updated", { historyId }); } } export default new ChatService();