refactor(sessions): move session summary generation to startup (#14691)

This commit is contained in:
Jack Wotherspoon
2025-12-09 22:25:22 -05:00
committed by GitHub
parent d2a6b30398
commit ee6556cbd2
6 changed files with 313 additions and 352 deletions

View File

@@ -498,6 +498,20 @@ export async function main() {
// Handle --list-sessions flag // Handle --list-sessions flag
if (config.getListSessions()) { if (config.getListSessions()) {
// Attempt auth for summary generation (gracefully skips if not configured)
const authType = settings.merged.security?.auth?.selectedType;
if (authType) {
try {
await config.refreshAuth(authType);
} catch (e) {
// Auth failed - continue without summary generation capability
debugLogger.debug(
'Auth failed for --list-sessions, summaries may not be generated:',
e,
);
}
}
await listSessions(config); await listSessions(config);
await runExitCleanup(); await runExitCleanup();
process.exit(ExitCodes.SUCCESS); process.exit(ExitCodes.SUCCESS);

View File

@@ -63,7 +63,7 @@ import {
SessionEndReason, SessionEndReason,
fireSessionStartHook, fireSessionStartHook,
fireSessionEndHook, fireSessionEndHook,
generateAndSaveSummary, generateSummary,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js'; import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process'; import process from 'node:process';
@@ -312,9 +312,13 @@ export const AppContainer = (props: AppContainerProps) => {
: SessionStartSource.Startup; : SessionStartSource.Startup;
await fireSessionStartHook(hookMessageBus, sessionStartSource); await fireSessionStartHook(hookMessageBus, sessionStartSource);
} }
// Fire-and-forget: generate summary for previous session in background
generateSummary(config).catch((e) => {
debugLogger.warn('Background summary generation failed:', e);
});
})(); })();
registerCleanup(async () => { registerCleanup(async () => {
await generateAndSaveSummary(config);
// Turn off mouse scroll. // Turn off mouse scroll.
disableMouseEvents(); disableMouseEvents();
const ideClient = await IdeClient.getInstance(); const ideClient = await IdeClient.getInstance();

View File

@@ -21,6 +21,7 @@ vi.mock('@google/gemini-cli-core', async () => {
return { return {
...actual, ...actual,
ChatRecordingService: vi.fn(), ChatRecordingService: vi.fn(),
generateSummary: vi.fn().mockResolvedValue(undefined),
}; };
}); });

View File

@@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { ChatRecordingService, type Config } from '@google/gemini-cli-core'; import {
ChatRecordingService,
generateSummary,
type Config,
} from '@google/gemini-cli-core';
import { import {
formatRelativeTime, formatRelativeTime,
SessionSelector, SessionSelector,
@@ -12,6 +16,9 @@ import {
} from './sessionUtils.js'; } from './sessionUtils.js';
export async function listSessions(config: Config): Promise<void> { export async function listSessions(config: Config): Promise<void> {
// Generate summary for most recent session if needed
await generateSummary(config);
const sessionSelector = new SessionSelector(config); const sessionSelector = new SessionSelector(config);
const sessions = await sessionSelector.listSessions(); const sessions = await sessionSelector.listSessions();

View File

@@ -5,11 +5,15 @@
*/ */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { generateAndSaveSummary } from './sessionSummaryUtils.js'; import { generateSummary, getPreviousSession } from './sessionSummaryUtils.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import type { ChatRecordingService } from './chatRecordingService.js';
import type { ContentGenerator } from '../core/contentGenerator.js'; import type { ContentGenerator } from '../core/contentGenerator.js';
import type { GeminiClient } from '../core/client.js'; import * as fs from 'node:fs/promises';
import * as path from 'node:path';
// Mock fs/promises
vi.mock('node:fs/promises');
const mockReaddir = fs.readdir as unknown as ReturnType<typeof vi.fn>;
// Mock the SessionSummaryService module // Mock the SessionSummaryService module
vi.mock('./sessionSummaryService.js', () => ({ vi.mock('./sessionSummaryService.js', () => ({
@@ -23,10 +27,24 @@ vi.mock('../core/baseLlmClient.js', () => ({
BaseLlmClient: vi.fn(), BaseLlmClient: vi.fn(),
})); }));
// Helper to create a session with N user messages
function createSessionWithUserMessages(
count: number,
options: { summary?: string; sessionId?: string } = {},
) {
return JSON.stringify({
sessionId: options.sessionId ?? 'session-id',
summary: options.summary,
messages: Array.from({ length: count }, (_, i) => ({
id: String(i + 1),
type: 'user',
content: [{ text: `Message ${i + 1}` }],
})),
});
}
describe('sessionSummaryUtils', () => { describe('sessionSummaryUtils', () => {
let mockConfig: Config; let mockConfig: Config;
let mockChatRecordingService: ChatRecordingService;
let mockGeminiClient: GeminiClient;
let mockContentGenerator: ContentGenerator; let mockContentGenerator: ContentGenerator;
let mockGenerateSummary: ReturnType<typeof vi.fn>; let mockGenerateSummary: ReturnType<typeof vi.fn>;
@@ -36,23 +54,12 @@ describe('sessionSummaryUtils', () => {
// Setup mock content generator // Setup mock content generator
mockContentGenerator = {} as ContentGenerator; mockContentGenerator = {} as ContentGenerator;
// Setup mock chat recording service
mockChatRecordingService = {
getConversation: vi.fn(),
saveSummary: vi.fn(),
} as unknown as ChatRecordingService;
// Setup mock gemini client
mockGeminiClient = {
getChatRecordingService: vi
.fn()
.mockReturnValue(mockChatRecordingService),
} as unknown as GeminiClient;
// Setup mock config // Setup mock config
mockConfig = { mockConfig = {
getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator), getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient), storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
} as unknown as Config; } as unknown as Config;
// Setup mock generateSummary function // Setup mock generateSummary function
@@ -73,320 +80,138 @@ describe('sessionSummaryUtils', () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
describe('Integration Tests', () => { describe('getPreviousSession', () => {
it('should generate and save summary successfully', async () => { it('should return null if chats directory does not exist', async () => {
const mockConversation = { vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT'));
sessionId: 'test-session',
projectHash: 'test-hash',
startTime: '2025-12-03T00:00:00Z',
lastUpdated: '2025-12-03T00:10:00Z',
messages: [
{
id: '1',
timestamp: '2025-12-03T00:00:00Z',
type: 'user' as const,
content: [{ text: 'How do I add dark mode?' }],
},
{
id: '2',
timestamp: '2025-12-03T00:01:00Z',
type: 'gemini' as const,
content: [{ text: 'To add dark mode...' }],
},
],
};
( const result = await getPreviousSession(mockConfig);
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
await generateAndSaveSummary(mockConfig); expect(result).toBeNull();
});
expect(mockChatRecordingService.getConversation).toHaveBeenCalledTimes(1); it('should return null if no session files exist', async () => {
expect(mockGenerateSummary).toHaveBeenCalledTimes(1); vi.mocked(fs.access).mockResolvedValue(undefined);
expect(mockGenerateSummary).toHaveBeenCalledWith({ mockReaddir.mockResolvedValue([]);
messages: mockConversation.messages,
}); const result = await getPreviousSession(mockConfig);
expect(mockChatRecordingService.saveSummary).toHaveBeenCalledTimes(1);
expect(mockChatRecordingService.saveSummary).toHaveBeenCalledWith( expect(result).toBeNull();
'Add dark mode to the app', });
it('should return null if most recent session already has summary', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
mockReaddir.mockResolvedValue(['session-2024-01-01T10-00-abc12345.json']);
vi.mocked(fs.readFile).mockResolvedValue(
createSessionWithUserMessages(5, { summary: 'Existing summary' }),
);
const result = await getPreviousSession(mockConfig);
expect(result).toBeNull();
});
it('should return null if most recent session has 1 or fewer user messages', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
mockReaddir.mockResolvedValue(['session-2024-01-01T10-00-abc12345.json']);
vi.mocked(fs.readFile).mockResolvedValue(
createSessionWithUserMessages(1),
);
const result = await getPreviousSession(mockConfig);
expect(result).toBeNull();
});
it('should return path if most recent session has more than 1 user message and no summary', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
mockReaddir.mockResolvedValue(['session-2024-01-01T10-00-abc12345.json']);
vi.mocked(fs.readFile).mockResolvedValue(
createSessionWithUserMessages(2),
);
const result = await getPreviousSession(mockConfig);
expect(result).toBe(
path.join(
'/tmp/project',
'chats',
'session-2024-01-01T10-00-abc12345.json',
),
); );
}); });
it('should skip if no chat recording service is available', async () => { it('should select most recently created session by filename', async () => {
( vi.mocked(fs.access).mockResolvedValue(undefined);
mockGeminiClient.getChatRecordingService as ReturnType<typeof vi.fn> mockReaddir.mockResolvedValue([
).mockReturnValue(undefined); 'session-2024-01-01T10-00-older000.json',
'session-2024-01-02T10-00-newer000.json',
]);
vi.mocked(fs.readFile).mockResolvedValue(
createSessionWithUserMessages(2),
);
await generateAndSaveSummary(mockConfig); const result = await getPreviousSession(mockConfig);
expect(mockGeminiClient.getChatRecordingService).toHaveBeenCalledTimes(1); expect(result).toBe(
expect(mockGenerateSummary).not.toHaveBeenCalled(); path.join(
expect(mockChatRecordingService.saveSummary).not.toHaveBeenCalled(); '/tmp/project',
'chats',
'session-2024-01-02T10-00-newer000.json',
),
);
}); });
it('should skip if no conversation exists', async () => { it('should return null if most recent session file is corrupted', async () => {
( vi.mocked(fs.access).mockResolvedValue(undefined);
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn> mockReaddir.mockResolvedValue(['session-2024-01-01T10-00-abc12345.json']);
).mockReturnValue(null); vi.mocked(fs.readFile).mockResolvedValue('invalid json');
await generateAndSaveSummary(mockConfig); const result = await getPreviousSession(mockConfig);
expect(mockChatRecordingService.getConversation).toHaveBeenCalledTimes(1); expect(result).toBeNull();
expect(mockGenerateSummary).not.toHaveBeenCalled();
expect(mockChatRecordingService.saveSummary).not.toHaveBeenCalled();
});
it('should skip if summary already exists', async () => {
const mockConversation = {
sessionId: 'test-session',
projectHash: 'test-hash',
startTime: '2025-12-03T00:00:00Z',
lastUpdated: '2025-12-03T00:10:00Z',
summary: 'Existing summary',
messages: [
{
id: '1',
timestamp: '2025-12-03T00:00:00Z',
type: 'user' as const,
content: [{ text: 'Hello' }],
},
],
};
(
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
await generateAndSaveSummary(mockConfig);
expect(mockChatRecordingService.getConversation).toHaveBeenCalledTimes(1);
expect(mockGenerateSummary).not.toHaveBeenCalled();
expect(mockChatRecordingService.saveSummary).not.toHaveBeenCalled();
});
it('should skip if no messages present', async () => {
const mockConversation = {
sessionId: 'test-session',
projectHash: 'test-hash',
startTime: '2025-12-03T00:00:00Z',
lastUpdated: '2025-12-03T00:10:00Z',
messages: [],
};
(
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
await generateAndSaveSummary(mockConfig);
expect(mockChatRecordingService.getConversation).toHaveBeenCalledTimes(1);
expect(mockGenerateSummary).not.toHaveBeenCalled();
expect(mockChatRecordingService.saveSummary).not.toHaveBeenCalled();
});
it('should handle generateSummary failure gracefully', async () => {
const mockConversation = {
sessionId: 'test-session',
projectHash: 'test-hash',
startTime: '2025-12-03T00:00:00Z',
lastUpdated: '2025-12-03T00:10:00Z',
messages: [
{
id: '1',
timestamp: '2025-12-03T00:00:00Z',
type: 'user' as const,
content: [{ text: 'Hello' }],
},
],
};
(
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
mockGenerateSummary.mockResolvedValue(null);
await generateAndSaveSummary(mockConfig);
expect(mockChatRecordingService.getConversation).toHaveBeenCalledTimes(1);
expect(mockGenerateSummary).toHaveBeenCalledTimes(1);
expect(mockChatRecordingService.saveSummary).not.toHaveBeenCalled();
});
it('should catch and log errors without throwing', async () => {
const mockConversation = {
sessionId: 'test-session',
projectHash: 'test-hash',
startTime: '2025-12-03T00:00:00Z',
lastUpdated: '2025-12-03T00:10:00Z',
messages: [
{
id: '1',
timestamp: '2025-12-03T00:00:00Z',
type: 'user' as const,
content: [{ text: 'Hello' }],
},
],
};
(
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
mockGenerateSummary.mockRejectedValue(new Error('API Error'));
// Should not throw
await expect(generateAndSaveSummary(mockConfig)).resolves.not.toThrow();
expect(mockChatRecordingService.getConversation).toHaveBeenCalledTimes(1);
expect(mockGenerateSummary).toHaveBeenCalledTimes(1);
expect(mockChatRecordingService.saveSummary).not.toHaveBeenCalled();
}); });
}); });
describe('Mock Verification Tests', () => { describe('generateSummary', () => {
it('should call getConversation() once', async () => { it('should not throw if getPreviousSession returns null', async () => {
const mockConversation = { vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT'));
sessionId: 'test-session',
projectHash: 'test-hash',
startTime: '2025-12-03T00:00:00Z',
lastUpdated: '2025-12-03T00:10:00Z',
messages: [
{
id: '1',
timestamp: '2025-12-03T00:00:00Z',
type: 'user' as const,
content: [{ text: 'Hello' }],
},
],
};
( await expect(generateSummary(mockConfig)).resolves.not.toThrow();
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
await generateAndSaveSummary(mockConfig);
expect(mockChatRecordingService.getConversation).toHaveBeenCalledTimes(1);
expect(mockChatRecordingService.getConversation).toHaveBeenCalledWith();
}); });
it('should call generateSummary() with correct messages', async () => { it('should generate and save summary for session needing one', async () => {
const mockMessages = [ const sessionPath = path.join(
{ '/tmp/project',
id: '1', 'chats',
timestamp: '2025-12-03T00:00:00Z', 'session-2024-01-01T10-00-abc12345.json',
type: 'user' as const, );
content: [{ text: 'How do I add dark mode?' }],
},
{
id: '2',
timestamp: '2025-12-03T00:01:00Z',
type: 'gemini' as const,
content: [{ text: 'To add dark mode...' }],
},
];
const mockConversation = { vi.mocked(fs.access).mockResolvedValue(undefined);
sessionId: 'test-session', mockReaddir.mockResolvedValue(['session-2024-01-01T10-00-abc12345.json']);
projectHash: 'test-hash', vi.mocked(fs.readFile).mockResolvedValue(
startTime: '2025-12-03T00:00:00Z', createSessionWithUserMessages(2),
lastUpdated: '2025-12-03T00:10:00Z', );
messages: mockMessages, vi.mocked(fs.writeFile).mockResolvedValue(undefined);
};
( await generateSummary(mockConfig);
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
await generateAndSaveSummary(mockConfig);
expect(mockGenerateSummary).toHaveBeenCalledTimes(1); expect(mockGenerateSummary).toHaveBeenCalledTimes(1);
expect(mockGenerateSummary).toHaveBeenCalledWith({ expect(fs.writeFile).toHaveBeenCalledTimes(1);
messages: mockMessages, expect(fs.writeFile).toHaveBeenCalledWith(
}); sessionPath,
}); expect.stringContaining('Add dark mode to the app'),
it('should call saveSummary() with generated summary', async () => {
const mockConversation = {
sessionId: 'test-session',
projectHash: 'test-hash',
startTime: '2025-12-03T00:00:00Z',
lastUpdated: '2025-12-03T00:10:00Z',
messages: [
{
id: '1',
timestamp: '2025-12-03T00:00:00Z',
type: 'user' as const,
content: [{ text: 'Hello' }],
},
],
};
(
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
mockGenerateSummary.mockResolvedValue('Test summary');
await generateAndSaveSummary(mockConfig);
expect(mockChatRecordingService.saveSummary).toHaveBeenCalledTimes(1);
expect(mockChatRecordingService.saveSummary).toHaveBeenCalledWith(
'Test summary',
); );
}); });
it('should not call saveSummary() if generation fails', async () => { it('should handle errors gracefully without throwing', async () => {
const mockConversation = { vi.mocked(fs.access).mockResolvedValue(undefined);
sessionId: 'test-session', mockReaddir.mockResolvedValue(['session-2024-01-01T10-00-abc12345.json']);
projectHash: 'test-hash', vi.mocked(fs.readFile).mockResolvedValue(
startTime: '2025-12-03T00:00:00Z', createSessionWithUserMessages(2),
lastUpdated: '2025-12-03T00:10:00Z', );
messages: [ mockGenerateSummary.mockRejectedValue(new Error('API Error'));
{
id: '1',
timestamp: '2025-12-03T00:00:00Z',
type: 'user' as const,
content: [{ text: 'Hello' }],
},
],
};
( await expect(generateSummary(mockConfig)).resolves.not.toThrow();
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
mockGenerateSummary.mockResolvedValue(null);
await generateAndSaveSummary(mockConfig);
expect(mockGenerateSummary).toHaveBeenCalledTimes(1);
expect(mockChatRecordingService.saveSummary).not.toHaveBeenCalled();
});
it('should not call saveSummary() if generateSummary throws', async () => {
const mockConversation = {
sessionId: 'test-session',
projectHash: 'test-hash',
startTime: '2025-12-03T00:00:00Z',
lastUpdated: '2025-12-03T00:10:00Z',
messages: [
{
id: '1',
timestamp: '2025-12-03T00:00:00Z',
type: 'user' as const,
content: [{ text: 'Hello' }],
},
],
};
(
mockChatRecordingService.getConversation as ReturnType<typeof vi.fn>
).mockReturnValue(mockConversation);
mockGenerateSummary.mockRejectedValue(new Error('Generation failed'));
await generateAndSaveSummary(mockConfig);
expect(mockGenerateSummary).toHaveBeenCalledTimes(1);
expect(mockChatRecordingService.saveSummary).not.toHaveBeenCalled();
}); });
}); });
}); });

View File

@@ -8,62 +8,172 @@ import type { Config } from '../config/config.js';
import { SessionSummaryService } from './sessionSummaryService.js'; import { SessionSummaryService } from './sessionSummaryService.js';
import { BaseLlmClient } from '../core/baseLlmClient.js'; import { BaseLlmClient } from '../core/baseLlmClient.js';
import { debugLogger } from '../utils/debugLogger.js'; import { debugLogger } from '../utils/debugLogger.js';
import {
SESSION_FILE_PREFIX,
type ConversationRecord,
} from './chatRecordingService.js';
import fs from 'node:fs/promises';
import path from 'node:path';
const MIN_MESSAGES_FOR_SUMMARY = 1;
/** /**
* Generates and saves a summary for the current session. * Generates and saves a summary for a session file.
* This is called during session cleanup and is non-blocking - errors are logged but don't prevent exit.
*/ */
export async function generateAndSaveSummary(config: Config): Promise<void> { async function generateAndSaveSummary(
config: Config,
sessionPath: string,
): Promise<void> {
// Read session file
const content = await fs.readFile(sessionPath, 'utf-8');
const conversation: ConversationRecord = JSON.parse(content);
// Skip if summary already exists
if (conversation.summary) {
debugLogger.debug(
`[SessionSummary] Summary already exists for ${sessionPath}, skipping`,
);
return;
}
// Skip if no messages
if (conversation.messages.length === 0) {
debugLogger.debug(
`[SessionSummary] No messages to summarize in ${sessionPath}`,
);
return;
}
// Create summary service
const contentGenerator = config.getContentGenerator();
if (!contentGenerator) {
debugLogger.debug(
'[SessionSummary] Content generator not available, skipping summary generation',
);
return;
}
const baseLlmClient = new BaseLlmClient(contentGenerator, config);
const summaryService = new SessionSummaryService(baseLlmClient);
// Generate summary
const summary = await summaryService.generateSummary({
messages: conversation.messages,
});
if (!summary) {
debugLogger.warn(
`[SessionSummary] Failed to generate summary for ${sessionPath}`,
);
return;
}
// Re-read the file before writing to handle race conditions
const freshContent = await fs.readFile(sessionPath, 'utf-8');
const freshConversation: ConversationRecord = JSON.parse(freshContent);
// Check if summary was added by another process
if (freshConversation.summary) {
debugLogger.debug(
`[SessionSummary] Summary was added by another process for ${sessionPath}`,
);
return;
}
// Add summary and write back
freshConversation.summary = summary;
freshConversation.lastUpdated = new Date().toISOString();
await fs.writeFile(sessionPath, JSON.stringify(freshConversation, null, 2));
debugLogger.debug(
`[SessionSummary] Saved summary for ${sessionPath}: "${summary}"`,
);
}
/**
* Finds the most recently created session that needs a summary.
* Returns the path if it needs a summary, null otherwise.
*/
export async function getPreviousSession(
config: Config,
): Promise<string | null> {
try { try {
// Get the chat recording service from config const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats');
const chatRecordingService = config
.getGeminiClient() // Check if chats directory exists
?.getChatRecordingService(); try {
if (!chatRecordingService) { await fs.access(chatsDir);
debugLogger.debug('[SessionSummary] No chat recording service available'); } catch {
return; debugLogger.debug('[SessionSummary] No chats directory found');
return null;
} }
// Get the current conversation // List session files
const conversation = chatRecordingService.getConversation(); const allFiles = await fs.readdir(chatsDir);
if (!conversation) { const sessionFiles = allFiles.filter(
debugLogger.debug('[SessionSummary] No conversation to summarize'); (f) => f.startsWith(SESSION_FILE_PREFIX) && f.endsWith('.json'),
return; );
if (sessionFiles.length === 0) {
debugLogger.debug('[SessionSummary] No session files found');
return null;
} }
// Skip if summary already exists (e.g., resumed session) // Sort by filename descending (most recently created first)
if (conversation.summary) { // Filename format: session-YYYY-MM-DDTHH-MM-XXXXXXXX.json
debugLogger.debug('[SessionSummary] Summary already exists, skipping'); sessionFiles.sort((a, b) => b.localeCompare(a));
return;
// Check the most recently created session
const mostRecentFile = sessionFiles[0];
const filePath = path.join(chatsDir, mostRecentFile);
try {
const content = await fs.readFile(filePath, 'utf-8');
const conversation: ConversationRecord = JSON.parse(content);
if (conversation.summary) {
debugLogger.debug(
'[SessionSummary] Most recent session already has summary',
);
return null;
}
// Only generate summaries for sessions with more than 1 user message
const userMessageCount = conversation.messages.filter(
(m) => m.type === 'user',
).length;
if (userMessageCount <= MIN_MESSAGES_FOR_SUMMARY) {
debugLogger.debug(
`[SessionSummary] Most recent session has ${userMessageCount} user message(s), skipping (need more than ${MIN_MESSAGES_FOR_SUMMARY})`,
);
return null;
}
return filePath;
} catch {
debugLogger.debug('[SessionSummary] Could not read most recent session');
return null;
} }
} catch (error) {
debugLogger.debug(
`[SessionSummary] Error finding previous session: ${error instanceof Error ? error.message : String(error)}`,
);
return null;
}
}
// Skip if no messages /**
if (conversation.messages.length === 0) { * Generates summary for the previous session if it lacks one.
debugLogger.debug('[SessionSummary] No messages to summarize'); * This is designed to be called fire-and-forget on startup.
return; */
} export async function generateSummary(config: Config): Promise<void> {
try {
// Create summary service const sessionPath = await getPreviousSession(config);
const contentGenerator = config.getContentGenerator(); if (sessionPath) {
const baseLlmClient = new BaseLlmClient(contentGenerator, config); await generateAndSaveSummary(config, sessionPath);
const summaryService = new SessionSummaryService(baseLlmClient);
// Generate summary
const summary = await summaryService.generateSummary({
messages: conversation.messages,
});
// Save summary if generated successfully
if (summary) {
chatRecordingService.saveSummary(summary);
debugLogger.debug(`[SessionSummary] Saved summary: "${summary}"`);
} else {
debugLogger.warn('[SessionSummary] Failed to generate summary');
} }
} catch (error) { } catch (error) {
// Log but don't throw - we want graceful degradation // Log but don't throw - we want graceful degradation
debugLogger.warn( debugLogger.warn(
`[SessionSummary] Error in generateAndSaveSummary: ${error instanceof Error ? error.message : String(error)}`, `[SessionSummary] Error generating summary: ${error instanceof Error ? error.message : String(error)}`,
); );
} }
} }