From e1994f6314352257e3ca9d82bab74d56c34b64fb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 10:05:14 +0000 Subject: [PATCH 1/6] =?UTF-8?q?perf:=20conversations=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E7=BA=AFDB=E6=9F=A5=E8=AF=A2=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E5=AE=9E=E6=97=B6=E8=A7=A3=E6=9E=90=E6=96=87?= =?UTF-8?q?=E4=BB=B6=20#AI=20commit#?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改动文件: - src/services/session-info-service.ts - src/services/claude-history-reader.ts - src/routes/conversation.routes.ts - src/cui-server.ts 改动点: - sessions表新增5列(project_path/summary/message_count/total_duration_ms/model),schema v3→v4自动迁移 - 对话启动时(POST /conversations/start)立即将project_path和model写入DB - result消息到达时将summary/message_count/total_duration写入DB - GET /conversations改为listConversationsFromDB()单次SQL查询,不再解析JSONL文件 - 服务启动时后台一次性迁移历史对话数据到DB(parseConversationChains) - 移除每次list请求末尾的syncMissingSessions冗余写入 --- src/cui-server.ts | 54 +++++++++- src/routes/conversation.routes.ts | 55 +++++----- src/services/claude-history-reader.ts | 7 ++ src/services/session-info-service.ts | 140 +++++++++++++++++++++++++- 4 files changed, 220 insertions(+), 36 deletions(-) diff --git a/src/cui-server.ts b/src/cui-server.ts index 45ddc2b..98cef63 100644 --- a/src/cui-server.ts +++ b/src/cui-server.ts @@ -25,10 +25,11 @@ import { WebPushService } from './services/web-push-service.js'; import { geminiService } from './services/gemini-service.js'; import { ClaudeRouterService } from './services/claude-router-service.js'; import { DmsUserTokenService } from './services/dms-user-token-service.js'; -import { +import { StreamEvent, CUIError, - PermissionRequest + PermissionRequest, + ResultStreamMessage } from './types/index.js'; import { createLogger, type Logger } from './services/logger.js'; import { createConversationRoutes } from './routes/conversation.routes.js'; @@ -282,6 +283,9 @@ export class CUIServer { this.logger.error('Failed to reload router after config change', error); } }); + + // Backfill historical conversation metadata into the DB (fire-and-forget) + this.runHistoricalMigration(); } catch (error) { this.logger.error('Failed to initialize server:', error, { errorType: error instanceof Error ? error.constructor.name : typeof error, @@ -603,7 +607,21 @@ export class CUIServer { }); return; } - + + // Persist completion metadata to DB when the result arrives + if (message && message.type === 'result') { + const resultMsg = message as ResultStreamMessage; + const sessionId = resultMsg.session_id; + if (sessionId) { + this.sessionInfoService.saveConversationEnd( + sessionId, + resultMsg.result || '', + resultMsg.num_turns * 2, + resultMsg.duration_ms + ); + } + } + // Stream other Claude messages as normal this.logger.debug('Broadcasting message to StreamManager', { streamingId, @@ -793,4 +811,34 @@ export class CUIServer { this.logger.error('Router initialization failed, continuing without router', error); } } + + /** + * Backfill project_path/model/summary into the DB for historical conversations + * that pre-date the DB-only approach. Runs once at startup, non-blocking. + */ + private runHistoricalMigration(): void { + this.historyReader.parseConversationChains() + .then(chains => { + let count = 0; + for (const chain of chains) { + if (!chain.projectPath && !chain.model) continue; + this.sessionInfoService.saveConversationStart(chain.sessionId, chain.projectPath, chain.model); + if (chain.summary || chain.totalDuration > 0) { + this.sessionInfoService.saveConversationEnd( + chain.sessionId, + chain.summary, + chain.messages.length, + chain.totalDuration + ); + } + count++; + } + this.logger.info('Historical conversation migration complete', { migrated: count, total: chains.length }); + }) + .catch(error => { + this.logger.warn('Historical conversation migration failed (non-fatal)', { + error: error instanceof Error ? error.message : String(error) + }); + }); + } } diff --git a/src/routes/conversation.routes.ts b/src/routes/conversation.routes.ts index 11d4545..b012d5f 100644 --- a/src/routes/conversation.routes.ts +++ b/src/routes/conversation.routes.ts @@ -123,7 +123,18 @@ export function createConversationRoutes( }; const { streamingId, systemInit } = await processManager.startConversation(conversationConfig); - + + // Persist initial conversation metadata to DB immediately (enables DB-only list) + try { + sessionInfoService.saveConversationStart(systemInit.session_id, systemInit.cwd, systemInit.model); + } catch (error) { + logger.warn('Failed to save conversation start metadata', { + requestId, + sessionId: systemInit.session_id, + error: error instanceof Error ? error.message : String(error) + }); + } + // Update original session with continuation session ID if resuming if (req.body.resumedSessionId) { try { @@ -250,65 +261,51 @@ export function createConversationRoutes( username, projectPath }; - const result = await historyReader.listConversations(queryWithProjectPath); - - // Update status for each conversation based on active streams + + // DB-only query — no file parsing + const result = sessionInfoService.listConversationsFromDB(queryWithProjectPath); + + // Overlay live status and toolMetrics for active streams const conversationsWithStatus = result.conversations.map(conversation => { const status = statusTracker.getConversationStatus(conversation.sessionId); - const baseConversation = { - ...conversation, - status - }; - - // Add toolMetrics if available + const baseConversation = { ...conversation, status }; + const metrics = toolMetricsService.getMetrics(conversation.sessionId); if (metrics) { baseConversation.toolMetrics = metrics; } - - // Add streamingId if conversation is ongoing + if (status === 'ongoing') { const streamingId = statusTracker.getStreamingId(conversation.sessionId); if (streamingId) { return { ...baseConversation, streamingId }; } } - + return baseConversation; }); - // Get all active sessions and add optimistic conversations for those not in history + // Include active conversations that haven't been flushed to the DB yet const existingSessionIds = new Set(conversationsWithStatus.map(c => c.sessionId)); const conversationsNotInHistory = conversationStatusManager.getConversationsNotInHistory( existingSessionIds, projectPath ); - // Combine history conversations with active ones not in history - var allConversations = [...conversationsWithStatus, ...conversationsNotInHistory]; - - // Ensure session info entries exist for all conversations - try { - await sessionInfoService.syncMissingSessions(allConversations.map(c => c.sessionId)); - } catch (syncError) { - logger.info('Failed to sync session info', { - requestId, - error: syncError instanceof Error ? syncError.message : String(syncError) - }); - } + const allConversations = [...conversationsWithStatus, ...conversationsNotInHistory]; logger.debug('Conversations listed successfully', { requestId, conversationCount: allConversations.length, - historyConversations: conversationsWithStatus.length, + dbConversations: conversationsWithStatus.length, conversationsNotInHistory: conversationsNotInHistory.length, totalFound: result.total, activeConversations: allConversations.filter(c => c.status === 'ongoing').length }); - + res.json({ conversations: allConversations, - total: allConversations.length // Update total to include conversations not in history + total: allConversations.length }); } catch (error) { logger.debug('List conversations failed', { diff --git a/src/services/claude-history-reader.ts b/src/services/claude-history-reader.ts index 82b3709..ce57f0f 100644 --- a/src/services/claude-history-reader.ts +++ b/src/services/claude-history-reader.ts @@ -309,6 +309,13 @@ export class ClaudeHistoryReader { return conversationChains; } + /** + * Parse all conversations from JSONL files without DB enrichment (used for startup migration) + */ + async parseConversationChains(username?: string): Promise { + return this.parseAllConversations(username); + } + /** * Parse all conversations from all JSONL files with file-level caching and concurrency protection */ diff --git a/src/services/session-info-service.ts b/src/services/session-info-service.ts index b77d8ef..cfc52a5 100644 --- a/src/services/session-info-service.ts +++ b/src/services/session-info-service.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; import Database from 'better-sqlite3'; -import type { SessionInfo } from '@/types/index.js'; +import type { SessionInfo, ConversationSummary, ConversationListQuery } from '@/types/index.js'; import { createLogger } from './logger.js'; import { type Logger } from './logger.js'; @@ -16,6 +16,12 @@ type SessionRow = { continuation_session_id: string; initial_commit_head: string; permission_mode: string; + // v4 columns + project_path: string; + summary: string; + message_count: number; + total_duration_ms: number; + model: string; }; /** @@ -40,6 +46,8 @@ export class SessionInfoService { private archiveAllStmt!: Database.Statement; private setMetadataStmt!: Database.Statement; private getMetadataStmt!: Database.Statement; + private saveConversationStartStmt!: Database.Statement; + private saveConversationEndStmt!: Database.Statement; constructor(customConfigDir?: string) { this.logger = createLogger('SessionInfoService'); @@ -113,8 +121,13 @@ export class SessionInfoService { ); `); - this.prepareStatements(); + // Prepare metadata statements first (needed for schema migration) + this.setMetadataStmt = this.db.prepare('INSERT INTO metadata (key, value) VALUES (@key, @value) ON CONFLICT(key) DO UPDATE SET value=excluded.value'); + this.getMetadataStmt = this.db.prepare('SELECT value FROM metadata WHERE key = ?'); + this.ensureMetadata(); + this.migrateSchema(); + this.prepareStatements(); this.isInitialized = true; } catch (error) { this.logger.error('Failed to initialize session info database', error); @@ -165,8 +178,30 @@ export class SessionInfoService { this.getAllStmt = this.db.prepare('SELECT * FROM sessions'); this.countStmt = this.db.prepare('SELECT COUNT(*) as count FROM sessions'); this.archiveAllStmt = this.db.prepare('UPDATE sessions SET archived=1, updated_at=@updated_at WHERE archived=0'); - this.setMetadataStmt = this.db.prepare('INSERT INTO metadata (key, value) VALUES (@key, @value) ON CONFLICT(key) DO UPDATE SET value=excluded.value'); - this.getMetadataStmt = this.db.prepare('SELECT value FROM metadata WHERE key = ?'); + + // v4: capture conversation metadata at write time + this.saveConversationStartStmt = this.db.prepare(` + INSERT INTO sessions ( + session_id, custom_name, created_at, updated_at, version, + pinned, archived, continuation_session_id, initial_commit_head, + permission_mode, project_path, model + ) VALUES ( + @session_id, '', @now, @now, 4, + 0, 0, '', '', 'default', @project_path, @model + ) + ON CONFLICT(session_id) DO UPDATE SET + project_path = CASE WHEN sessions.project_path = '' THEN excluded.project_path ELSE sessions.project_path END, + model = CASE WHEN sessions.model = '' THEN excluded.model ELSE sessions.model END, + updated_at = excluded.updated_at + `); + this.saveConversationEndStmt = this.db.prepare(` + UPDATE sessions SET + summary = @summary, + message_count = @message_count, + total_duration_ms = @total_duration_ms, + updated_at = @now + WHERE session_id = @session_id + `); } private ensureMetadata(): void { @@ -179,6 +214,25 @@ export class SessionInfoService { } } + private migrateSchema(): void { + const row = this.getMetadataStmt.get('schema_version') as { value?: string } | undefined; + const version = row ? parseInt(row.value || '0', 10) : 0; + if (version >= 4) return; + + const newColumns = [ + "ALTER TABLE sessions ADD COLUMN project_path TEXT NOT NULL DEFAULT ''", + "ALTER TABLE sessions ADD COLUMN summary TEXT NOT NULL DEFAULT ''", + "ALTER TABLE sessions ADD COLUMN message_count INTEGER NOT NULL DEFAULT 0", + "ALTER TABLE sessions ADD COLUMN total_duration_ms INTEGER NOT NULL DEFAULT 0", + "ALTER TABLE sessions ADD COLUMN model TEXT NOT NULL DEFAULT ''" + ]; + for (const sql of newColumns) { + try { this.db.exec(sql); } catch { /* column already exists */ } + } + this.setMetadataStmt.run({ key: 'schema_version', value: '4' }); + this.logger.info('Migrated sessions DB schema to v4'); + } + private mapRow(row: SessionRow): SessionInfo { return { custom_name: row.custom_name, @@ -396,6 +450,84 @@ export class SessionInfoService { } } + saveConversationStart(sessionId: string, projectPath: string, model: string): void { + try { + const now = new Date().toISOString(); + this.saveConversationStartStmt.run({ session_id: sessionId, now, project_path: projectPath, model }); + this.setMetadataStmt.run({ key: 'last_updated', value: now }); + } catch (error) { + this.logger.error('Failed to save conversation start', { sessionId, error }); + } + } + + saveConversationEnd(sessionId: string, summary: string, messageCount: number, totalDurationMs: number): void { + try { + const now = new Date().toISOString(); + this.saveConversationEndStmt.run({ session_id: sessionId, summary, message_count: messageCount, total_duration_ms: totalDurationMs, now }); + this.setMetadataStmt.run({ key: 'last_updated', value: now }); + } catch (error) { + this.logger.error('Failed to save conversation end', { sessionId, error }); + } + } + + listConversationsFromDB(filter?: ConversationListQuery): { conversations: ConversationSummary[]; total: number } { + try { + const conditions: string[] = []; + const params: Record = {}; + + if (filter?.projectPath) { + conditions.push('project_path = @project_path'); + params.project_path = filter.projectPath; + } + if (filter?.archived !== undefined) { + conditions.push('archived = @archived'); + params.archived = filter.archived ? 1 : 0; + } + if (filter?.pinned !== undefined) { + conditions.push('pinned = @pinned'); + params.pinned = filter.pinned ? 1 : 0; + } + if (filter?.hasContinuation !== undefined) { + conditions.push(filter.hasContinuation + ? "continuation_session_id != ''" + : "continuation_session_id = ''" + ); + } + + const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; + const sortField = filter?.sortBy === 'created' ? 'created_at' : 'updated_at'; + const sortOrder = filter?.order === 'asc' ? 'ASC' : 'DESC'; + const limit = filter?.limit ?? 20; + const offset = filter?.offset ?? 0; + + const countResult = this.db + .prepare(`SELECT COUNT(*) as count FROM sessions ${where}`) + .get(params) as { count: number }; + + const rows = this.db + .prepare(`SELECT * FROM sessions ${where} ORDER BY ${sortField} ${sortOrder} LIMIT @limit OFFSET @offset`) + .all({ ...params, limit, offset }) as Array; + + const conversations: ConversationSummary[] = rows.map(row => ({ + sessionId: row.session_id, + projectPath: row.project_path || '', + summary: row.summary || '', + sessionInfo: this.mapRow(row), + createdAt: row.created_at, + updatedAt: row.updated_at, + messageCount: row.message_count || 0, + totalDuration: row.total_duration_ms || 0, + model: row.model || '', + status: 'completed' as const + })); + + return { conversations, total: countResult.count }; + } catch (error) { + this.logger.error('Failed to list conversations from DB', error); + return { conversations: [], total: 0 }; + } + } + async syncMissingSessions(sessionIds: string[]): Promise { try { const now = new Date().toISOString(); From 9317a8718bb0a938bec2adf325e7e39b8283f179 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 10:05:40 +0000 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0config/dev.json?= =?UTF-8?q?=E5=92=8Cpackage-lock.json=20#AI=20commit#?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/dev.json | 22 ++++++++++++---------- package-lock.json | 1 - 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/config/dev.json b/config/dev.json index 97cf9dc..282b34e 100644 --- a/config/dev.json +++ b/config/dev.json @@ -1,12 +1,14 @@ { - "linkis": { - "token": "[*key_linkis_client_auth_token]", - "requestUrl": "[@GLOBAL_LINKIS_GZ_BDAP_DEV_GATEWAY_URL]" - }, - "dms": { - "appid": "[*key_dms_appid]", - "token": "[*key_dms_token]", - "baseUrl": "[*key_dms_base_url]", - "systemUsername": "[*key_dms_system_username]" - } + "linkis": { + "token": "[*key_linkis_client_auth_token]", + "requestUrl": "[@GLOBAL_LINKIS_GZ_BDAP_DEV_GATEWAY_URL]" + }, + "dms": { + "appid": "[*key_dms_appid]", + "token": "[*key_dms_token]", + "baseUrl": "[*key_dms_base_url]", + "systemUsername": "[*key_dms_system_username]" + }, + "machine_id": "vm-f6298bff015cb6c5", + "authToken": "085a169792c65190c43fa2deaf6cf8ab" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 64982aa..2cd9236 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "cui-server", "version": "0.6.3", - "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@anthropic-ai/claude-code": "^2.1.9", From cb29268d6b6b3e5a3a5d23e7b590e806fb146a91 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 12:09:08 +0000 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E6=9C=AA?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9A=84total=5Fduration=5Fms=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=20#AI=20commit#?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cui-server.ts | 8 +++----- src/services/session-info-service.ts | 15 ++++++--------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/cui-server.ts b/src/cui-server.ts index 98cef63..60e46e4 100644 --- a/src/cui-server.ts +++ b/src/cui-server.ts @@ -616,8 +616,7 @@ export class CUIServer { this.sessionInfoService.saveConversationEnd( sessionId, resultMsg.result || '', - resultMsg.num_turns * 2, - resultMsg.duration_ms + resultMsg.num_turns * 2 ); } } @@ -823,12 +822,11 @@ export class CUIServer { for (const chain of chains) { if (!chain.projectPath && !chain.model) continue; this.sessionInfoService.saveConversationStart(chain.sessionId, chain.projectPath, chain.model); - if (chain.summary || chain.totalDuration > 0) { + if (chain.summary) { this.sessionInfoService.saveConversationEnd( chain.sessionId, chain.summary, - chain.messages.length, - chain.totalDuration + chain.messages.length ); } count++; diff --git a/src/services/session-info-service.ts b/src/services/session-info-service.ts index cfc52a5..9aba7a8 100644 --- a/src/services/session-info-service.ts +++ b/src/services/session-info-service.ts @@ -20,7 +20,6 @@ type SessionRow = { project_path: string; summary: string; message_count: number; - total_duration_ms: number; model: string; }; @@ -196,10 +195,9 @@ export class SessionInfoService { `); this.saveConversationEndStmt = this.db.prepare(` UPDATE sessions SET - summary = @summary, - message_count = @message_count, - total_duration_ms = @total_duration_ms, - updated_at = @now + summary = @summary, + message_count = @message_count, + updated_at = @now WHERE session_id = @session_id `); } @@ -223,7 +221,6 @@ export class SessionInfoService { "ALTER TABLE sessions ADD COLUMN project_path TEXT NOT NULL DEFAULT ''", "ALTER TABLE sessions ADD COLUMN summary TEXT NOT NULL DEFAULT ''", "ALTER TABLE sessions ADD COLUMN message_count INTEGER NOT NULL DEFAULT 0", - "ALTER TABLE sessions ADD COLUMN total_duration_ms INTEGER NOT NULL DEFAULT 0", "ALTER TABLE sessions ADD COLUMN model TEXT NOT NULL DEFAULT ''" ]; for (const sql of newColumns) { @@ -460,10 +457,10 @@ export class SessionInfoService { } } - saveConversationEnd(sessionId: string, summary: string, messageCount: number, totalDurationMs: number): void { + saveConversationEnd(sessionId: string, summary: string, messageCount: number): void { try { const now = new Date().toISOString(); - this.saveConversationEndStmt.run({ session_id: sessionId, summary, message_count: messageCount, total_duration_ms: totalDurationMs, now }); + this.saveConversationEndStmt.run({ session_id: sessionId, summary, message_count: messageCount, now }); this.setMetadataStmt.run({ key: 'last_updated', value: now }); } catch (error) { this.logger.error('Failed to save conversation end', { sessionId, error }); @@ -516,7 +513,7 @@ export class SessionInfoService { createdAt: row.created_at, updatedAt: row.updated_at, messageCount: row.message_count || 0, - totalDuration: row.total_duration_ms || 0, + totalDuration: 0, model: row.model || '', status: 'completed' as const })); From ae7cf70bd716bea62da2d5ccaa9e0c28e73ddf45 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 12:12:02 +0000 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=E5=AE=8C=E5=85=A8=E7=A7=BB?= =?UTF-8?q?=E9=99=A4totalDuration=E5=AD=97=E6=AE=B5=20#AI=20commit#?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/conversation.routes.ts | 1 - src/services/claude-history-reader.ts | 8 +------- src/services/conversation-cache.ts | 1 - src/services/conversation-status-manager.ts | 2 -- src/services/session-info-service.ts | 1 - src/types/index.ts | 2 -- 6 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/routes/conversation.routes.ts b/src/routes/conversation.routes.ts index b012d5f..5a3e791 100644 --- a/src/routes/conversation.routes.ts +++ b/src/routes/conversation.routes.ts @@ -344,7 +344,6 @@ export function createConversationRoutes( summary: metadata.summary, projectPath: metadata.projectPath, metadata: { - totalDuration: metadata.totalDuration, model: metadata.model } }; diff --git a/src/services/claude-history-reader.ts b/src/services/claude-history-reader.ts index ce57f0f..44a6741 100644 --- a/src/services/claude-history-reader.ts +++ b/src/services/claude-history-reader.ts @@ -112,7 +112,6 @@ export class ClaudeHistoryReader { createdAt: chain.createdAt, updatedAt: chain.updatedAt, messageCount: chain.messages.length, - totalDuration: chain.totalDuration, model: chain.model, status: 'completed' as const, // Default status, will be updated by server toolMetrics: toolMetrics @@ -160,7 +159,6 @@ export class ClaudeHistoryReader { summary: string; projectPath: string; model: string; - totalDuration: number; } | null> { try { const conversationChains = await this.parseAllConversations(username); @@ -173,8 +171,7 @@ export class ClaudeHistoryReader { return { summary: conversation.summary, projectPath: conversation.projectPath, - model: conversation.model, - totalDuration: conversation.totalDuration + model: conversation.model }; } catch (error) { this.logger.error('Error getting metadata for conversation', error, { sessionId }); @@ -476,8 +473,6 @@ export class ClaudeHistoryReader { // Determine conversation summary const summary = this.determineConversationSummary(filteredMessages, summaries); - // Calculate metadata from filtered messages - const totalDuration = filteredMessages.reduce((sum, msg) => sum + (msg.durationMs || 0), 0); const model = this.extractModel(filteredMessages); // Get timestamps from filtered messages @@ -496,7 +491,6 @@ export class ClaudeHistoryReader { summary, createdAt, updatedAt, - totalDuration, model }; } catch (error) { diff --git a/src/services/conversation-cache.ts b/src/services/conversation-cache.ts index b607d4b..9c16100 100644 --- a/src/services/conversation-cache.ts +++ b/src/services/conversation-cache.ts @@ -9,7 +9,6 @@ export interface ConversationChain { summary: string; createdAt: string; updatedAt: string; - totalDuration: number; model: string; } diff --git a/src/services/conversation-status-manager.ts b/src/services/conversation-status-manager.ts index af49d5b..650de39 100644 --- a/src/services/conversation-status-manager.ts +++ b/src/services/conversation-status-manager.ts @@ -249,7 +249,6 @@ export class ConversationStatusManager extends EventEmitter { createdAt: context.timestamp, updatedAt: context.timestamp, messageCount: 1, // At least the initial user message - totalDuration: 0, // No duration yet model: context.model || 'unknown', status: 'ongoing' as const, streamingId @@ -323,7 +322,6 @@ export class ConversationStatusManager extends EventEmitter { summary: '', // No summary for active conversation projectPath: context.workingDirectory, metadata: { - totalDuration: 0, model: context.model || 'unknown' } }; diff --git a/src/services/session-info-service.ts b/src/services/session-info-service.ts index 9aba7a8..6d731c6 100644 --- a/src/services/session-info-service.ts +++ b/src/services/session-info-service.ts @@ -513,7 +513,6 @@ export class SessionInfoService { createdAt: row.created_at, updatedAt: row.updated_at, messageCount: row.message_count || 0, - totalDuration: 0, model: row.model || '', status: 'completed' as const })); diff --git a/src/types/index.ts b/src/types/index.ts index 86fe3cb..55a8573 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -18,7 +18,6 @@ export interface ConversationSummary { createdAt: string; updatedAt: string; messageCount: number; - totalDuration: number; model: string; status: 'completed' | 'ongoing' | 'pending'; // Conversation status based on active streams streamingId?: string; // CUI's internal streaming ID (only present when status is 'ongoing') @@ -157,7 +156,6 @@ export interface ConversationDetailsResponse { summary: string; projectPath: string; metadata: { - totalDuration: number; model: string; }; toolMetrics?: ToolMetrics; // Optional tool usage metrics From 37f1a5402c0775f8215771f3f67f899f8961d4a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 12:19:19 +0000 Subject: [PATCH 5/6] refactor: remove message_count and model from sessions DB layer #AI commit# MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改动文件: - src/services/session-info-service.ts - src/cui-server.ts - src/routes/conversation.routes.ts 改动点: - saveConversationEnd 签名去掉 messageCount 参数 - listConversationsFromDB 返回固定值 messageCount:0, model:'' 而非读DB列 - 历史迁移和新会话调用点同步去掉多余参数 --- src/cui-server.ts | 13 ++++--------- src/routes/conversation.routes.ts | 2 +- src/services/session-info-service.ts | 26 ++++++++++---------------- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/cui-server.ts b/src/cui-server.ts index 60e46e4..e0dd5e8 100644 --- a/src/cui-server.ts +++ b/src/cui-server.ts @@ -615,8 +615,7 @@ export class CUIServer { if (sessionId) { this.sessionInfoService.saveConversationEnd( sessionId, - resultMsg.result || '', - resultMsg.num_turns * 2 + resultMsg.result || '' ); } } @@ -820,14 +819,10 @@ export class CUIServer { .then(chains => { let count = 0; for (const chain of chains) { - if (!chain.projectPath && !chain.model) continue; - this.sessionInfoService.saveConversationStart(chain.sessionId, chain.projectPath, chain.model); + if (!chain.projectPath) continue; + this.sessionInfoService.saveConversationStart(chain.sessionId, chain.projectPath); if (chain.summary) { - this.sessionInfoService.saveConversationEnd( - chain.sessionId, - chain.summary, - chain.messages.length - ); + this.sessionInfoService.saveConversationEnd(chain.sessionId, chain.summary); } count++; } diff --git a/src/routes/conversation.routes.ts b/src/routes/conversation.routes.ts index 5a3e791..6abba2c 100644 --- a/src/routes/conversation.routes.ts +++ b/src/routes/conversation.routes.ts @@ -126,7 +126,7 @@ export function createConversationRoutes( // Persist initial conversation metadata to DB immediately (enables DB-only list) try { - sessionInfoService.saveConversationStart(systemInit.session_id, systemInit.cwd, systemInit.model); + sessionInfoService.saveConversationStart(systemInit.session_id, systemInit.cwd); } catch (error) { logger.warn('Failed to save conversation start metadata', { requestId, diff --git a/src/services/session-info-service.ts b/src/services/session-info-service.ts index 6d731c6..763b528 100644 --- a/src/services/session-info-service.ts +++ b/src/services/session-info-service.ts @@ -19,8 +19,6 @@ type SessionRow = { // v4 columns project_path: string; summary: string; - message_count: number; - model: string; }; /** @@ -183,21 +181,19 @@ export class SessionInfoService { INSERT INTO sessions ( session_id, custom_name, created_at, updated_at, version, pinned, archived, continuation_session_id, initial_commit_head, - permission_mode, project_path, model + permission_mode, project_path ) VALUES ( @session_id, '', @now, @now, 4, - 0, 0, '', '', 'default', @project_path, @model + 0, 0, '', '', 'default', @project_path ) ON CONFLICT(session_id) DO UPDATE SET project_path = CASE WHEN sessions.project_path = '' THEN excluded.project_path ELSE sessions.project_path END, - model = CASE WHEN sessions.model = '' THEN excluded.model ELSE sessions.model END, updated_at = excluded.updated_at `); this.saveConversationEndStmt = this.db.prepare(` UPDATE sessions SET - summary = @summary, - message_count = @message_count, - updated_at = @now + summary = @summary, + updated_at = @now WHERE session_id = @session_id `); } @@ -220,8 +216,6 @@ export class SessionInfoService { const newColumns = [ "ALTER TABLE sessions ADD COLUMN project_path TEXT NOT NULL DEFAULT ''", "ALTER TABLE sessions ADD COLUMN summary TEXT NOT NULL DEFAULT ''", - "ALTER TABLE sessions ADD COLUMN message_count INTEGER NOT NULL DEFAULT 0", - "ALTER TABLE sessions ADD COLUMN model TEXT NOT NULL DEFAULT ''" ]; for (const sql of newColumns) { try { this.db.exec(sql); } catch { /* column already exists */ } @@ -447,20 +441,20 @@ export class SessionInfoService { } } - saveConversationStart(sessionId: string, projectPath: string, model: string): void { + saveConversationStart(sessionId: string, projectPath: string): void { try { const now = new Date().toISOString(); - this.saveConversationStartStmt.run({ session_id: sessionId, now, project_path: projectPath, model }); + this.saveConversationStartStmt.run({ session_id: sessionId, now, project_path: projectPath }); this.setMetadataStmt.run({ key: 'last_updated', value: now }); } catch (error) { this.logger.error('Failed to save conversation start', { sessionId, error }); } } - saveConversationEnd(sessionId: string, summary: string, messageCount: number): void { + saveConversationEnd(sessionId: string, summary: string): void { try { const now = new Date().toISOString(); - this.saveConversationEndStmt.run({ session_id: sessionId, summary, message_count: messageCount, now }); + this.saveConversationEndStmt.run({ session_id: sessionId, summary, now }); this.setMetadataStmt.run({ key: 'last_updated', value: now }); } catch (error) { this.logger.error('Failed to save conversation end', { sessionId, error }); @@ -512,8 +506,8 @@ export class SessionInfoService { sessionInfo: this.mapRow(row), createdAt: row.created_at, updatedAt: row.updated_at, - messageCount: row.message_count || 0, - model: row.model || '', + messageCount: 0, + model: '', status: 'completed' as const })); From aa718a552599e8bfb74a5bf90ebc285630ef9ea2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 12:29:49 +0000 Subject: [PATCH 6/6] refactor: remove message_count from entire project including UI #AI commit# MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改动文件: - src/types/index.ts - src/services/session-info-service.ts - src/services/conversation-status-manager.ts - src/services/claude-history-reader.ts - src/web/chat/components/Home/TaskList.tsx - src/web/chat/components/Home/TaskItem.tsx - tests/unit/* 改动点: - ConversationSummary 类型移除 messageCount 字段 - 所有生产者(DB查询/历史解析/活跃会话)不再产出 messageCount - 会话列表 UI(TaskItem/TaskList)移除消息数显示 - 清理测试中的 messageCount 引用 --- src/services/claude-history-reader.ts | 1 - src/services/conversation-status-manager.ts | 1 - src/services/session-info-service.ts | 1 - src/types/index.ts | 1 - src/web/chat/components/Home/TaskItem.tsx | 8 -------- src/web/chat/components/Home/TaskList.tsx | 1 - tests/unit/claude-history-reader.test.ts | 12 ++++-------- tests/unit/cui-server.test.ts | 2 -- .../services/WorkingDirectoriesService.test.ts | 14 +++----------- 9 files changed, 7 insertions(+), 34 deletions(-) diff --git a/src/services/claude-history-reader.ts b/src/services/claude-history-reader.ts index 44a6741..d7d34d4 100644 --- a/src/services/claude-history-reader.ts +++ b/src/services/claude-history-reader.ts @@ -111,7 +111,6 @@ export class ClaudeHistoryReader { sessionInfo: sessionInfo, createdAt: chain.createdAt, updatedAt: chain.updatedAt, - messageCount: chain.messages.length, model: chain.model, status: 'completed' as const, // Default status, will be updated by server toolMetrics: toolMetrics diff --git a/src/services/conversation-status-manager.ts b/src/services/conversation-status-manager.ts index 650de39..f61d633 100644 --- a/src/services/conversation-status-manager.ts +++ b/src/services/conversation-status-manager.ts @@ -248,7 +248,6 @@ export class ConversationStatusManager extends EventEmitter { }, createdAt: context.timestamp, updatedAt: context.timestamp, - messageCount: 1, // At least the initial user message model: context.model || 'unknown', status: 'ongoing' as const, streamingId diff --git a/src/services/session-info-service.ts b/src/services/session-info-service.ts index 763b528..2f6ff2d 100644 --- a/src/services/session-info-service.ts +++ b/src/services/session-info-service.ts @@ -506,7 +506,6 @@ export class SessionInfoService { sessionInfo: this.mapRow(row), createdAt: row.created_at, updatedAt: row.updated_at, - messageCount: 0, model: '', status: 'completed' as const })); diff --git a/src/types/index.ts b/src/types/index.ts index 55a8573..ef86cc0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,7 +17,6 @@ export interface ConversationSummary { sessionInfo: SessionInfo; // Complete session metadata from SessionInfoService createdAt: string; updatedAt: string; - messageCount: number; model: string; status: 'completed' | 'ongoing' | 'pending'; // Conversation status based on active streams streamingId?: string; // CUI's internal streaming ID (only present when status is 'ongoing') diff --git a/src/web/chat/components/Home/TaskItem.tsx b/src/web/chat/components/Home/TaskItem.tsx index fba2845..0fb2cc6 100644 --- a/src/web/chat/components/Home/TaskItem.tsx +++ b/src/web/chat/components/Home/TaskItem.tsx @@ -14,7 +14,6 @@ interface TaskItemProps { projectPath: string; recentDirectories: Record; status: 'ongoing' | 'completed' | 'error' | 'pending'; - messageCount?: number; toolMetrics?: { linesAdded: number; linesRemoved: number; @@ -39,7 +38,6 @@ export function TaskItem({ projectPath, recentDirectories, status, - messageCount, toolMetrics, liveStatus, isArchived = false, @@ -173,12 +171,6 @@ export function TaskItem({ ? (recentDirectories[projectPath]?.shortname || projectPath.split('/').pop() || projectPath) : 'No project'} - {messageCount !== undefined && ( - <> - · - {messageCount} - - )} diff --git a/src/web/chat/components/Home/TaskList.tsx b/src/web/chat/components/Home/TaskList.tsx index 8a45717..2627f4d 100644 --- a/src/web/chat/components/Home/TaskList.tsx +++ b/src/web/chat/components/Home/TaskList.tsx @@ -201,7 +201,6 @@ export function TaskList({ projectPath={conversation.projectPath} recentDirectories={recentDirectories} status={conversation.status} - messageCount={conversation.messageCount} toolMetrics={conversation.toolMetrics} liveStatus={conversation.liveStatus} isArchived={activeTab === 'archive'} diff --git a/tests/unit/claude-history-reader.test.ts b/tests/unit/claude-history-reader.test.ts index 8ebec4a..104582f 100644 --- a/tests/unit/claude-history-reader.test.ts +++ b/tests/unit/claude-history-reader.test.ts @@ -143,7 +143,6 @@ describe('ClaudeHistoryReader', () => { expect(conversation.sessionId).toBe(sessionId); expect(conversation.projectPath).toBe('/Users/username/project'); // Should use cwd from message expect(conversation.summary).toBe('Example Development Session'); // Should use summary linked via leafUuid - expect(conversation.messageCount).toBe(2); // user + assistant message expect(conversation.createdAt).toBeDefined(); expect(conversation.updatedAt).toBeDefined(); // Total cost calculation has been removed @@ -614,7 +613,7 @@ describe('ClaudeHistoryReader', () => { describe('filter and pagination utilities', () => { it('should handle null/undefined filters', () => { const conversations = [ - { sessionId: '1', projectPath: '/test', summary: 'Test', createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', messageCount: 1 } + { sessionId: '1', projectPath: '/test', summary: 'Test', createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z' } ]; const result = (reader as any).applyFilters(conversations, undefined); @@ -630,8 +629,7 @@ describe('ClaudeHistoryReader', () => { projectPath: '/test', summary: `Test ${i}`, createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - messageCount: 1 + updatedAt: new Date().toISOString() })); const result = (reader as any).applyPagination(conversations, {}); @@ -645,16 +643,14 @@ describe('ClaudeHistoryReader', () => { projectPath: '/test', summary: 'Test 1', createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-03T00:00:00Z', - messageCount: 1 + updatedAt: '2024-01-03T00:00:00Z' }, { sessionId: '2', projectPath: '/test', summary: 'Test 2', createdAt: '2024-01-02T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - messageCount: 1 + updatedAt: '2024-01-01T00:00:00Z' } ]; diff --git a/tests/unit/cui-server.test.ts b/tests/unit/cui-server.test.ts index 00f34ed..cc481e0 100644 --- a/tests/unit/cui-server.test.ts +++ b/tests/unit/cui-server.test.ts @@ -742,7 +742,6 @@ describe('CUIServer', () => { summary: 'First conversation', createdAt: '2024-01-01T10:00:00Z', updatedAt: '2024-01-01T10:30:00Z', - messageCount: 5, totalCost: 0.0023, totalDuration: 1500, model: 'claude-sonnet-3-5-20241022', @@ -754,7 +753,6 @@ describe('CUIServer', () => { summary: 'Second conversation', createdAt: '2024-01-02T14:00:00Z', updatedAt: '2024-01-02T15:30:00Z', - messageCount: 8, totalCost: 0.0045, totalDuration: 3000, model: 'claude-opus-20240229', diff --git a/tests/unit/services/WorkingDirectoriesService.test.ts b/tests/unit/services/WorkingDirectoriesService.test.ts index dba1b95..71c1c80 100644 --- a/tests/unit/services/WorkingDirectoriesService.test.ts +++ b/tests/unit/services/WorkingDirectoriesService.test.ts @@ -24,8 +24,6 @@ const createConversation = (overrides: Partial): Conversati }, createdAt: '2024-01-01T10:00:00Z', updatedAt: '2024-01-01T10:00:00Z', - messageCount: 1, - totalDuration: 100, model: 'claude-3', status: 'completed' as const, ...overrides @@ -65,27 +63,21 @@ describe('WorkingDirectoriesService', () => { projectPath: '/home/user/project1', summary: 'Test 1', createdAt: '2024-01-01T10:00:00Z', - updatedAt: '2024-01-01T10:00:00Z', - messageCount: 1, - totalDuration: 100 + updatedAt: '2024-01-01T10:00:00Z' }), createConversation({ sessionId: '2', projectPath: '/home/user/project1', summary: 'Test 2', createdAt: '2024-01-02T10:00:00Z', - updatedAt: '2024-01-02T10:00:00Z', - messageCount: 2, - totalDuration: 200 + updatedAt: '2024-01-02T10:00:00Z' }), createConversation({ sessionId: '3', projectPath: '/home/user/project2', summary: 'Test 3', createdAt: '2024-01-03T10:00:00Z', - updatedAt: '2024-01-03T10:00:00Z', - messageCount: 1, - totalDuration: 150 + updatedAt: '2024-01-03T10:00:00Z' }) ];