From 3c07fef6d3fa7853d311c20af782d531ea6696b0 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 4 May 2026 12:45:08 +0300 Subject: [PATCH] fix(session-synchronizer): improve session name extraction for Claude and Codex --- .../claude-session-synchronizer.provider.ts | 65 ++++++++++++++++++- .../codex-session-synchronizer.provider.ts | 64 +++++++++++++++++- 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts b/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts index 7d089a2d..66f055fd 100644 --- a/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +++ b/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts @@ -1,5 +1,6 @@ import os from 'node:os'; import path from 'node:path'; +import { readFile } from 'node:fs/promises'; import { sessionsDb } from '@/modules/database/index.js'; import { @@ -91,7 +92,7 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { filePath: string, nameMap: Map ): Promise { - return extractFirstValidJsonlData(filePath, (rawData) => { + const parsed = await extractFirstValidJsonlData(filePath, (rawData) => { const data = rawData as Record; const sessionId = typeof data.sessionId === 'string' ? data.sessionId : undefined; const projectPath = typeof data.cwd === 'string' ? data.cwd : undefined; @@ -103,8 +104,68 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { return { sessionId, projectPath, - sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Claude Session'), }; }); + + if (!parsed) { + return null; + } + + const existingSession = sessionsDb.getSessionById(parsed.sessionId); + const existingSessionName = existingSession?.custom_name; + if (existingSessionName && existingSessionName !== 'Untitled Claude Session') { + return { + ...parsed, + sessionName: normalizeSessionName(existingSessionName, 'Untitled Claude Session'), + }; + } + + let sessionName = nameMap.get(parsed.sessionId); + if (!sessionName) { + sessionName = await this.extractSessionAiTitleFromEnd(filePath, parsed.sessionId); + } + + return { + ...parsed, + sessionName: normalizeSessionName(sessionName, 'Untitled Claude Session'), + }; + } + + private async extractSessionAiTitleFromEnd( + filePath: string, + sessionId: string + ): Promise { + try { + const content = await readFile(filePath, 'utf8'); + const lines = content.split(/\r?\n/); + + for (let index = lines.length - 1; index >= 0; index -= 1) { + const line = lines[index]?.trim(); + if (!line) { + continue; + } + + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch { + continue; + } + + const data = parsed as Record; + const eventType = typeof data.type === 'string' ? data.type : undefined; + const eventSessionId = typeof data.sessionId === 'string' ? data.sessionId : undefined; + const aiTitle = typeof data.aiTitle === 'string' ? data.aiTitle : undefined; + const lastPrompt = typeof data.lastPrompt === 'string' ? data.lastPrompt : undefined; + + if ((eventType === 'ai-title' && eventSessionId === sessionId && aiTitle?.trim()) || (eventType === 'last-prompt' && eventSessionId === sessionId && lastPrompt?.trim())) { + return aiTitle || lastPrompt; + } + } + } catch { + // Ignore missing/unreadable files so sync can continue. + } + + return undefined; } } diff --git a/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts b/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts index bd1edc0c..0e8025ef 100644 --- a/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +++ b/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts @@ -1,5 +1,6 @@ import os from 'node:os'; import path from 'node:path'; +import { readFile } from 'node:fs/promises'; import { sessionsDb } from '@/modules/database/index.js'; import { @@ -99,7 +100,7 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer { filePath: string, nameMap: Map ): Promise { - return extractFirstValidJsonlData(filePath, (rawData) => { + const parsed = await extractFirstValidJsonlData(filePath, (rawData) => { const data = rawData as Record; const payload = data.payload as Record | undefined; const sessionId = typeof payload?.id === 'string' ? payload.id : undefined; @@ -112,8 +113,67 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer { return { sessionId, projectPath, - sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Codex Session'), }; }); + + if (!parsed) { + return null; + } + + const existingSession = sessionsDb.getSessionById(parsed.sessionId); + const existingSessionName = existingSession?.custom_name; + if (existingSessionName && existingSessionName !== 'Untitled Codex Session') { + return { + ...parsed, + sessionName: normalizeSessionName(existingSessionName, 'Untitled Codex Session'), + }; + } + + let sessionName = nameMap.get(parsed.sessionId); + if (!sessionName) { + sessionName = await this.extractLastAgentMessageFromEnd(filePath); + } + + return { + ...parsed, + sessionName: normalizeSessionName(sessionName, 'Untitled Codex Session'), + }; + } + + private async extractLastAgentMessageFromEnd(filePath: string): Promise { + try { + const content = await readFile(filePath, 'utf8'); + const lines = content.split(/\r?\n/); + + for (let index = lines.length - 1; index >= 0; index -= 1) { + const line = lines[index]?.trim(); + if (!line) { + continue; + } + + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch { + continue; + } + + const data = parsed as Record; + const eventType = typeof data.type === 'string' ? data.type : undefined; + const payload = data.payload as Record | undefined; + const payloadType = typeof payload?.type === 'string' ? payload.type : undefined; + const lastAgentMessage = typeof payload?.last_agent_message === 'string' + ? payload.last_agent_message + : undefined; + + if (eventType === 'event_msg' && payloadType === 'task_complete' && lastAgentMessage?.trim()) { + return lastAgentMessage; + } + } + } catch { + // Ignore missing/unreadable files so sync can continue. + } + + return undefined; } }