refactor: Centralize session ID generation and propagation

This commit is contained in:
jerop 2025-06-11 04:46:39 +00:00 committed by Jerop Kipruto
parent 95fdc66e7d
commit d1e23b7c71
20 changed files with 96 additions and 71 deletions

View File

@ -120,6 +120,7 @@ export async function loadCliConfig(
settings: Settings, settings: Settings,
extensions: ExtensionConfig[], extensions: ExtensionConfig[],
geminiIgnorePatterns: string[], geminiIgnorePatterns: string[],
sessionId: string,
): Promise<Config> { ): Promise<Config> {
loadEnvironment(); loadEnvironment();
@ -148,6 +149,7 @@ export async function loadCliConfig(
const mcpServers = mergeMcpServers(settings, extensions); const mcpServers = mergeMcpServers(settings, extensions);
return new Config({ return new Config({
sessionId,
contentGeneratorConfig, contentGeneratorConfig,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
sandbox: argv.sandbox ?? settings.sandbox, sandbox: argv.sandbox ?? settings.sandbox,

View File

@ -31,6 +31,7 @@ import {
WebFetchTool, WebFetchTool,
WebSearchTool, WebSearchTool,
WriteFileTool, WriteFileTool,
sessionId,
} from '@gemini-cli/core'; } from '@gemini-cli/core';
export async function main() { export async function main() {
@ -57,6 +58,7 @@ export async function main() {
settings.merged, settings.merged,
extensions, extensions,
geminiIgnorePatterns, geminiIgnorePatterns,
sessionId,
); );
// Initialize centralized FileDiscoveryService // Initialize centralized FileDiscoveryService
@ -180,5 +182,6 @@ async function loadNonInteractiveConfig(
nonInteractiveSettings, nonInteractiveSettings,
extensions, extensions,
config.getGeminiIgnorePatterns(), config.getGeminiIgnorePatterns(),
config.getSessionId(),
); );
} }

View File

@ -190,6 +190,7 @@ describe('App UI', () => {
userMemory: '', userMemory: '',
geminiMdFileCount: 0, geminiMdFileCount: 0,
showMemoryUsage: false, showMemoryUsage: false,
sessionId: 'test-session-id',
// Provide other required fields for ConfigParameters if necessary // Provide other required fields for ConfigParameters if necessary
}) as unknown as MockServerConfig; }) as unknown as MockServerConfig;

View File

@ -493,7 +493,7 @@ Add any other context about the problem here.
description: 'save conversation checkpoint. Usage: /save [tag]', description: 'save conversation checkpoint. Usage: /save [tag]',
action: async (_mainCommand, subCommand, _args) => { action: async (_mainCommand, subCommand, _args) => {
const tag = (subCommand || '').trim(); const tag = (subCommand || '').trim();
const logger = new Logger(); const logger = new Logger(config?.getSessionId() || '');
await logger.initialize(); await logger.initialize();
const chat = await config?.getGeminiClient()?.getChat(); const chat = await config?.getGeminiClient()?.getChat();
const history = chat?.getHistory() || []; const history = chat?.getHistory() || [];
@ -519,7 +519,7 @@ Add any other context about the problem here.
'resume from conversation checkpoint. Usage: /resume [tag]', 'resume from conversation checkpoint. Usage: /resume [tag]',
action: async (_mainCommand, subCommand, _args) => { action: async (_mainCommand, subCommand, _args) => {
const tag = (subCommand || '').trim(); const tag = (subCommand || '').trim();
const logger = new Logger(); const logger = new Logger(config?.getSessionId() || '');
await logger.initialize(); await logger.initialize();
const conversation = await logger.loadCheckpoint(tag); const conversation = await logger.loadCheckpoint(tag);
if (conversation.length === 0) { if (conversation.length === 0) {

View File

@ -5,6 +5,7 @@
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { sessionId } from '@gemini-cli/core';
import { Logger } from '@gemini-cli/core'; import { Logger } from '@gemini-cli/core';
/** /**
@ -14,7 +15,7 @@ export const useLogger = () => {
const [logger, setLogger] = useState<Logger | null>(null); const [logger, setLogger] = useState<Logger | null>(null);
useEffect(() => { useEffect(() => {
const newLogger = new Logger(); const newLogger = new Logger(sessionId);
/** /**
* Start async initialization, no need to await. Using await slows down the * Start async initialization, no need to await. Using await slows down the
* time from launch to see the gemini-cli prompt and it's better to not save * time from launch to see the gemini-cli prompt and it's better to not save

View File

@ -49,6 +49,7 @@ describe('Server Config (config.ts)', () => {
const USER_MEMORY = 'Test User Memory'; const USER_MEMORY = 'Test User Memory';
const TELEMETRY = false; const TELEMETRY = false;
const EMBEDDING_MODEL = 'gemini-embedding'; const EMBEDDING_MODEL = 'gemini-embedding';
const SESSION_ID = 'test-session-id';
const baseParams: ConfigParameters = { const baseParams: ConfigParameters = {
contentGeneratorConfig: { contentGeneratorConfig: {
apiKey: API_KEY, apiKey: API_KEY,
@ -62,6 +63,7 @@ describe('Server Config (config.ts)', () => {
fullContext: FULL_CONTEXT, fullContext: FULL_CONTEXT,
userMemory: USER_MEMORY, userMemory: USER_MEMORY,
telemetry: TELEMETRY, telemetry: TELEMETRY,
sessionId: SESSION_ID,
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -55,6 +55,7 @@ export class MCPServerConfig {
} }
export interface ConfigParameters { export interface ConfigParameters {
sessionId: string;
contentGeneratorConfig: ContentGeneratorConfig; contentGeneratorConfig: ContentGeneratorConfig;
embeddingModel: string; embeddingModel: string;
sandbox?: boolean | string; sandbox?: boolean | string;
@ -83,6 +84,7 @@ export interface ConfigParameters {
export class Config { export class Config {
private toolRegistry: Promise<ToolRegistry>; private toolRegistry: Promise<ToolRegistry>;
private readonly sessionId: string;
private readonly contentGeneratorConfig: ContentGeneratorConfig; private readonly contentGeneratorConfig: ContentGeneratorConfig;
private readonly embeddingModel: string; private readonly embeddingModel: string;
private readonly sandbox: boolean | string | undefined; private readonly sandbox: boolean | string | undefined;
@ -111,6 +113,7 @@ export class Config {
private fileDiscoveryService: FileDiscoveryService | null = null; private fileDiscoveryService: FileDiscoveryService | null = null;
constructor(params: ConfigParameters) { constructor(params: ConfigParameters) {
this.sessionId = params.sessionId;
this.contentGeneratorConfig = params.contentGeneratorConfig; this.contentGeneratorConfig = params.contentGeneratorConfig;
this.embeddingModel = params.embeddingModel; this.embeddingModel = params.embeddingModel;
this.sandbox = params.sandbox; this.sandbox = params.sandbox;
@ -155,6 +158,10 @@ export class Config {
} }
} }
getSessionId(): string {
return this.sessionId;
}
getContentGeneratorConfig(): ContentGeneratorConfig { getContentGeneratorConfig(): ContentGeneratorConfig {
return this.contentGeneratorConfig; return this.contentGeneratorConfig;
} }

View File

@ -108,6 +108,7 @@ describe('Gemini Client (client.ts)', () => {
getUserAgent: vi.fn().mockReturnValue('test-agent'), getUserAgent: vi.fn().mockReturnValue('test-agent'),
getUserMemory: vi.fn().mockReturnValue(''), getUserMemory: vi.fn().mockReturnValue(''),
getFullContext: vi.fn().mockReturnValue(false), getFullContext: vi.fn().mockReturnValue(false),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return mock as any; return mock as any;

View File

@ -162,6 +162,7 @@ export class GeminiClient {
return new GeminiChat( return new GeminiChat(
await this.contentGenerator, await this.contentGenerator,
this.model, this.model,
this.config.getSessionId(),
{ {
systemInstruction, systemInstruction,
...this.generateContentConfig, ...this.generateContentConfig,

View File

@ -21,11 +21,12 @@ describe('GeminiChat', () => {
let chat: GeminiChat; let chat: GeminiChat;
const model = 'gemini-pro'; const model = 'gemini-pro';
const config: GenerateContentConfig = {}; const config: GenerateContentConfig = {};
const sessionId = 'test-session-id';
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
// Reset history for each test by creating a new instance // Reset history for each test by creating a new instance
chat = new GeminiChat(mockModelsModule, model, config, []); chat = new GeminiChat(mockModelsModule, model, sessionId, config, []);
}); });
afterEach(() => { afterEach(() => {
@ -120,7 +121,7 @@ describe('GeminiChat', () => {
chat.recordHistory(userInput, newModelOutput); // userInput here is for the *next* turn, but history is already primed chat.recordHistory(userInput, newModelOutput); // userInput here is for the *next* turn, but history is already primed
// Reset and set up a more realistic scenario for merging with existing history // Reset and set up a more realistic scenario for merging with existing history
chat = new GeminiChat(mockModelsModule, model, config, []); chat = new GeminiChat(mockModelsModule, model, sessionId, config, []);
const firstUserInput: Content = { const firstUserInput: Content = {
role: 'user', role: 'user',
parts: [{ text: 'First user input' }], parts: [{ text: 'First user input' }],
@ -163,7 +164,7 @@ describe('GeminiChat', () => {
role: 'model', role: 'model',
parts: [{ text: 'Initial model answer.' }], parts: [{ text: 'Initial model answer.' }],
}; };
chat = new GeminiChat(mockModelsModule, model, config, [ chat = new GeminiChat(mockModelsModule, model, sessionId, config, [
initialUser, initialUser,
initialModel, initialModel,
]); ]);

View File

@ -18,6 +18,7 @@ import {
import { retryWithBackoff } from '../utils/retry.js'; import { retryWithBackoff } from '../utils/retry.js';
import { isFunctionResponse } from '../utils/messageInspectors.js'; import { isFunctionResponse } from '../utils/messageInspectors.js';
import { ContentGenerator } from './contentGenerator.js'; import { ContentGenerator } from './contentGenerator.js';
import { Logger } from './logger.js';
/** /**
* Returns true if the response is valid, false otherwise. * Returns true if the response is valid, false otherwise.
@ -117,14 +118,17 @@ export class GeminiChat {
// A promise to represent the current state of the message being sent to the // A promise to represent the current state of the message being sent to the
// model. // model.
private sendPromise: Promise<void> = Promise.resolve(); private sendPromise: Promise<void> = Promise.resolve();
private logger: Logger;
constructor( constructor(
private readonly contentGenerator: ContentGenerator, private readonly contentGenerator: ContentGenerator,
private readonly model: string, private readonly model: string,
sessionId: string,
private readonly config: GenerateContentConfig = {}, private readonly config: GenerateContentConfig = {},
private history: Content[] = [], private history: Content[] = [],
) { ) {
validateHistory(history); validateHistory(history);
this.logger = new Logger(sessionId);
} }
/** /**

View File

@ -78,15 +78,20 @@ async function readLogFile(): Promise<LogEntry[]> {
} }
} }
vi.mock('../utils/session.js', () => ({
sessionId: 'test-session-id',
}));
describe('Logger', () => { describe('Logger', () => {
let logger: Logger; let logger: Logger;
const testSessionId = 'test-session-id';
beforeEach(async () => { beforeEach(async () => {
vi.resetAllMocks(); vi.resetAllMocks();
vi.useFakeTimers(); vi.useFakeTimers();
vi.setSystemTime(new Date('2025-01-01T12:00:00.000Z')); vi.setSystemTime(new Date('2025-01-01T12:00:00.000Z'));
await cleanupLogFile(); await cleanupLogFile();
logger = new Logger(); logger = new Logger(testSessionId);
await logger.initialize(); await logger.initialize();
}); });
@ -111,7 +116,7 @@ describe('Logger', () => {
/* ignore */ /* ignore */
} }
const newLogger = new Logger(); const newLogger = new Logger(testSessionId);
await newLogger.initialize(); await newLogger.initialize();
const dirExists = await fs const dirExists = await fs
@ -130,9 +135,8 @@ describe('Logger', () => {
}); });
it('should load existing logs and set correct messageId for the current session', async () => { it('should load existing logs and set correct messageId for the current session', async () => {
const fixedTime = new Date('2025-01-01T10:00:00.000Z'); const currentSessionId = 'session-123';
vi.setSystemTime(fixedTime); const anotherSessionId = 'session-456';
const currentSessionId = Math.floor(fixedTime.getTime() / 1000);
const existingLogs: LogEntry[] = [ const existingLogs: LogEntry[] = [
{ {
sessionId: currentSessionId, sessionId: currentSessionId,
@ -142,7 +146,7 @@ describe('Logger', () => {
message: 'Msg1', message: 'Msg1',
}, },
{ {
sessionId: currentSessionId - 100, sessionId: anotherSessionId,
messageId: 5, messageId: 5,
timestamp: new Date('2025-01-01T09:00:00.000Z').toISOString(), timestamp: new Date('2025-01-01T09:00:00.000Z').toISOString(),
type: MessageSenderType.USER, type: MessageSenderType.USER,
@ -158,7 +162,7 @@ describe('Logger', () => {
]; ];
await fs.mkdir(path.join(process.cwd(), GEMINI_DIR), { recursive: true }); await fs.mkdir(path.join(process.cwd(), GEMINI_DIR), { recursive: true });
await fs.writeFile(TEST_LOG_FILE_PATH, JSON.stringify(existingLogs)); await fs.writeFile(TEST_LOG_FILE_PATH, JSON.stringify(existingLogs));
const newLogger = new Logger(); const newLogger = new Logger(currentSessionId);
await newLogger.initialize(); await newLogger.initialize();
expect(newLogger['messageId']).toBe(2); expect(newLogger['messageId']).toBe(2);
expect(newLogger['logs']).toEqual(existingLogs); expect(newLogger['logs']).toEqual(existingLogs);
@ -166,11 +170,9 @@ describe('Logger', () => {
}); });
it('should set messageId to 0 for a new session if log file exists but has no logs for current session', async () => { it('should set messageId to 0 for a new session if log file exists but has no logs for current session', async () => {
const fixedTime = new Date('2025-01-01T14:00:00.000Z');
vi.setSystemTime(fixedTime);
const existingLogs: LogEntry[] = [ const existingLogs: LogEntry[] = [
{ {
sessionId: Math.floor(fixedTime.getTime() / 1000) - 500, sessionId: 'some-other-session',
messageId: 5, messageId: 5,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
type: MessageSenderType.USER, type: MessageSenderType.USER,
@ -179,7 +181,7 @@ describe('Logger', () => {
]; ];
await fs.mkdir(path.join(process.cwd(), GEMINI_DIR), { recursive: true }); await fs.mkdir(path.join(process.cwd(), GEMINI_DIR), { recursive: true });
await fs.writeFile(TEST_LOG_FILE_PATH, JSON.stringify(existingLogs)); await fs.writeFile(TEST_LOG_FILE_PATH, JSON.stringify(existingLogs));
const newLogger = new Logger(); const newLogger = new Logger('a-new-session');
await newLogger.initialize(); await newLogger.initialize();
expect(newLogger['messageId']).toBe(0); expect(newLogger['messageId']).toBe(0);
newLogger.close(); newLogger.close();
@ -203,7 +205,7 @@ describe('Logger', () => {
const consoleDebugSpy = vi const consoleDebugSpy = vi
.spyOn(console, 'debug') .spyOn(console, 'debug')
.mockImplementation(() => {}); .mockImplementation(() => {});
const newLogger = new Logger(); const newLogger = new Logger(testSessionId);
await newLogger.initialize(); await newLogger.initialize();
expect(consoleDebugSpy).toHaveBeenCalledWith( expect(consoleDebugSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid JSON in log file'), expect.stringContaining('Invalid JSON in log file'),
@ -232,7 +234,7 @@ describe('Logger', () => {
const consoleDebugSpy = vi const consoleDebugSpy = vi
.spyOn(console, 'debug') .spyOn(console, 'debug')
.mockImplementation(() => {}); .mockImplementation(() => {});
const newLogger = new Logger(); const newLogger = new Logger(testSessionId);
await newLogger.initialize(); await newLogger.initialize();
expect(consoleDebugSpy).toHaveBeenCalledWith( expect(consoleDebugSpy).toHaveBeenCalledWith(
`Log file at ${TEST_LOG_FILE_PATH} is not a valid JSON array. Starting with empty logs.`, `Log file at ${TEST_LOG_FILE_PATH} is not a valid JSON array. Starting with empty logs.`,
@ -259,7 +261,7 @@ describe('Logger', () => {
const logsFromFile = await readLogFile(); const logsFromFile = await readLogFile();
expect(logsFromFile.length).toBe(1); expect(logsFromFile.length).toBe(1);
expect(logsFromFile[0]).toMatchObject({ expect(logsFromFile[0]).toMatchObject({
sessionId: logger['sessionId'], sessionId: testSessionId,
messageId: 0, messageId: 0,
type: MessageSenderType.USER, type: MessageSenderType.USER,
message: 'Hello, world!', message: 'Hello, world!',
@ -283,7 +285,8 @@ describe('Logger', () => {
}); });
it('should handle logger not initialized', async () => { it('should handle logger not initialized', async () => {
const uninitializedLogger = new Logger(); const uninitializedLogger = new Logger(testSessionId);
uninitializedLogger.close(); // Ensure it's treated as uninitialized
const consoleDebugSpy = vi const consoleDebugSpy = vi
.spyOn(console, 'debug') .spyOn(console, 'debug')
.mockImplementation(() => {}); .mockImplementation(() => {});
@ -296,15 +299,13 @@ describe('Logger', () => {
}); });
it('should simulate concurrent writes from different logger instances to the same file', async () => { it('should simulate concurrent writes from different logger instances to the same file', async () => {
const logger1 = new Logger(); // logger1 const concurrentSessionId = 'concurrent-session';
vi.setSystemTime(new Date('2025-01-01T13:00:00.000Z')); const logger1 = new Logger(concurrentSessionId);
await logger1.initialize(); await logger1.initialize();
const s1 = logger1['sessionId'];
const logger2 = new Logger(); // logger2, will share same session if time is same const logger2 = new Logger(concurrentSessionId);
vi.setSystemTime(new Date('2025-01-01T13:00:00.000Z'));
await logger2.initialize(); await logger2.initialize();
expect(logger2['sessionId']).toEqual(s1); expect(logger2['sessionId']).toEqual(logger1['sessionId']);
// Log from logger1 // Log from logger1
await logger1.logMessage(MessageSenderType.USER, 'L1M1'); // L1 internal msgId becomes 1, writes {s1, 0} await logger1.logMessage(MessageSenderType.USER, 'L1M1'); // L1 internal msgId becomes 1, writes {s1, 0}
@ -361,43 +362,34 @@ describe('Logger', () => {
}); });
describe('getPreviousUserMessages', () => { describe('getPreviousUserMessages', () => {
it('should retrieve user messages, sorted newest first by session, then timestamp, then messageId', async () => { it('should retrieve all user messages from logs, sorted newest first', async () => {
const loggerSort = new Logger(); // This test now verifies that messages from different sessions are included
vi.setSystemTime(new Date('2025-01-01T10:00:00.000Z')); // and sorted correctly by timestamp, as the session-based sorting was removed.
const loggerSort = new Logger('session-1');
await loggerSort.initialize(); await loggerSort.initialize();
const s1 = loggerSort['sessionId']!;
await loggerSort.logMessage(MessageSenderType.USER, 'S1M0_ts100000'); // msgId 0 await loggerSort.logMessage(MessageSenderType.USER, 'S1M0_ts100000'); // msgId 0
vi.advanceTimersByTime(10); vi.advanceTimersByTime(1000);
await loggerSort.logMessage(MessageSenderType.USER, 'S1M1_ts100010'); // msgId 1 await loggerSort.logMessage(MessageSenderType.USER, 'S1M1_ts101000'); // msgId 1
loggerSort.close(); // Close to ensure next initialize starts a new session if time changed vi.advanceTimersByTime(1000);
await loggerSort.logMessage(MessageSenderType.USER, 'S2M0_ts102000'); // msgId 0 for s2
vi.setSystemTime(new Date('2025-01-01T11:00:00.000Z')); vi.advanceTimersByTime(1000);
await loggerSort.initialize(); // Re-initialize for a new session
const s2 = loggerSort['sessionId']!;
expect(s2).not.toEqual(s1);
await loggerSort.logMessage(MessageSenderType.USER, 'S2M0_ts110000'); // msgId 0 for s2
vi.advanceTimersByTime(10);
await loggerSort.logMessage( await loggerSort.logMessage(
'model' as MessageSenderType, 'model' as MessageSenderType,
'S2_Model_ts110010', 'S2_Model_ts103000',
); );
vi.advanceTimersByTime(10); vi.advanceTimersByTime(1000);
await loggerSort.logMessage(MessageSenderType.USER, 'S2M1_ts110020'); // msgId 1 for s2 await loggerSort.logMessage(MessageSenderType.USER, 'S2M1_ts104000'); // msgId 1 for s2
loggerSort.close(); loggerSort.close();
// To test the sorting thoroughly, especially the session part, we'll read the file // A new logger will load all previous logs regardless of session
// as if it was written by multiple sessions and then initialize a new logger to load them. const finalLogger = new Logger('final-session');
const combinedLogs = await readLogFile(); await finalLogger.initialize();
const finalLogger = new Logger();
// Manually set its internal logs to simulate loading from a file with mixed sessions
finalLogger['logs'] = combinedLogs;
finalLogger['initialized'] = true; // Mark as initialized to allow getPreviousUserMessages to run
const messages = await finalLogger.getPreviousUserMessages(); const messages = await finalLogger.getPreviousUserMessages();
expect(messages).toEqual([ expect(messages).toEqual([
'S2M1_ts110020', 'S2M1_ts104000',
'S2M0_ts110000', 'S2M0_ts102000',
'S1M1_ts100010', 'S1M1_ts101000',
'S1M0_ts100000', 'S1M0_ts100000',
]); ]);
finalLogger.close(); finalLogger.close();
@ -410,7 +402,8 @@ describe('Logger', () => {
}); });
it('should return empty array if logger not initialized', async () => { it('should return empty array if logger not initialized', async () => {
const uninitializedLogger = new Logger(); const uninitializedLogger = new Logger(testSessionId);
uninitializedLogger.close();
const messages = await uninitializedLogger.getPreviousUserMessages(); const messages = await uninitializedLogger.getPreviousUserMessages();
expect(messages).toEqual([]); expect(messages).toEqual([]);
uninitializedLogger.close(); uninitializedLogger.close();
@ -443,7 +436,8 @@ describe('Logger', () => {
}); });
it('should not throw if logger is not initialized', async () => { it('should not throw if logger is not initialized', async () => {
const uninitializedLogger = new Logger(); const uninitializedLogger = new Logger(testSessionId);
uninitializedLogger.close();
const consoleErrorSpy = vi const consoleErrorSpy = vi
.spyOn(console, 'error') .spyOn(console, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
@ -521,7 +515,8 @@ describe('Logger', () => {
}); });
it('should return an empty array if logger is not initialized', async () => { it('should return an empty array if logger is not initialized', async () => {
const uninitializedLogger = new Logger(); const uninitializedLogger = new Logger(testSessionId);
uninitializedLogger.close();
const consoleErrorSpy = vi const consoleErrorSpy = vi
.spyOn(console, 'error') .spyOn(console, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});

View File

@ -17,7 +17,7 @@ export enum MessageSenderType {
} }
export interface LogEntry { export interface LogEntry {
sessionId: number; sessionId: string;
messageId: number; messageId: number;
timestamp: string; timestamp: string;
type: MessageSenderType; type: MessageSenderType;
@ -28,12 +28,14 @@ export class Logger {
private geminiDir: string | undefined; private geminiDir: string | undefined;
private logFilePath: string | undefined; private logFilePath: string | undefined;
private checkpointFilePath: string | undefined; private checkpointFilePath: string | undefined;
private sessionId: number | undefined; private sessionId: string | undefined;
private messageId = 0; // Instance-specific counter for the next messageId private messageId = 0; // Instance-specific counter for the next messageId
private initialized = false; private initialized = false;
private logs: LogEntry[] = []; // In-memory cache, ideally reflects the last known state of the file private logs: LogEntry[] = []; // In-memory cache, ideally reflects the last known state of the file
constructor() {} constructor(sessionId: string) {
this.sessionId = sessionId;
}
private async _readLogFile(): Promise<LogEntry[]> { private async _readLogFile(): Promise<LogEntry[]> {
if (!this.logFilePath) { if (!this.logFilePath) {
@ -51,7 +53,7 @@ export class Logger {
} }
return parsedLogs.filter( return parsedLogs.filter(
(entry) => (entry) =>
typeof entry.sessionId === 'number' && typeof entry.sessionId === 'string' &&
typeof entry.messageId === 'number' && typeof entry.messageId === 'number' &&
typeof entry.timestamp === 'string' && typeof entry.timestamp === 'string' &&
typeof entry.type === 'string' && typeof entry.type === 'string' &&
@ -93,7 +95,6 @@ export class Logger {
if (this.initialized) { if (this.initialized) {
return; return;
} }
this.sessionId = Math.floor(Date.now() / 1000);
this.geminiDir = path.resolve(process.cwd(), GEMINI_DIR); this.geminiDir = path.resolve(process.cwd(), GEMINI_DIR);
this.logFilePath = path.join(this.geminiDir, LOG_FILE_NAME); this.logFilePath = path.join(this.geminiDir, LOG_FILE_NAME);
this.checkpointFilePath = path.join(this.geminiDir, CHECKPOINT_FILE_NAME); this.checkpointFilePath = path.join(this.geminiDir, CHECKPOINT_FILE_NAME);
@ -195,11 +196,9 @@ export class Logger {
return this.logs return this.logs
.filter((entry) => entry.type === MessageSenderType.USER) .filter((entry) => entry.type === MessageSenderType.USER)
.sort((a, b) => { .sort((a, b) => {
if (b.sessionId !== a.sessionId) return b.sessionId - a.sessionId;
const dateA = new Date(a.timestamp).getTime(); const dateA = new Date(a.timestamp).getTime();
const dateB = new Date(b.timestamp).getTime(); const dateB = new Date(b.timestamp).getTime();
if (dateB !== dateA) return dateB - dateA; return dateB - dateA;
return b.messageId - a.messageId;
}) })
.map((entry) => entry.message); .map((entry) => entry.message);
} }

View File

@ -51,3 +51,4 @@ export * from './tools/mcp-tool.js';
// Export telemetry functions // Export telemetry functions
export * from './telemetry/index.js'; export * from './telemetry/index.js';
export { sessionId } from './utils/session.js';

View File

@ -4,10 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { randomUUID } from 'crypto';
export const SERVICE_NAME = 'gemini-cli'; export const SERVICE_NAME = 'gemini-cli';
export const sessionId = randomUUID();
export const EVENT_USER_PROMPT = 'gemini_code.user_prompt'; export const EVENT_USER_PROMPT = 'gemini_code.user_prompt';
export const EVENT_TOOL_CALL = 'gemini_code.tool_call'; export const EVENT_TOOL_CALL = 'gemini_code.tool_call';

View File

@ -28,4 +28,3 @@ export {
} from './types.js'; } from './types.js';
export { SpanStatusCode, ValueType } from '@opentelemetry/api'; export { SpanStatusCode, ValueType } from '@opentelemetry/api';
export { SemanticAttributes } from '@opentelemetry/semantic-conventions'; export { SemanticAttributes } from '@opentelemetry/semantic-conventions';
export { sessionId } from './constants.js';

View File

@ -26,7 +26,7 @@ import {
} from '@opentelemetry/sdk-metrics'; } from '@opentelemetry/sdk-metrics';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { Config } from '../config/config.js'; import { Config } from '../config/config.js';
import { SERVICE_NAME, sessionId } from './constants.js'; import { SERVICE_NAME } from './constants.js';
import { initializeMetrics } from './metrics.js'; import { initializeMetrics } from './metrics.js';
import { logCliConfiguration } from './loggers.js'; import { logCliConfiguration } from './loggers.js';
@ -68,7 +68,7 @@ export function initializeTelemetry(config: Config): void {
const resource = new Resource({ const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
[SemanticResourceAttributes.SERVICE_VERSION]: process.version, [SemanticResourceAttributes.SERVICE_VERSION]: process.version,
'session.id': sessionId, 'session.id': config.getSessionId(),
}); });
const otlpEndpoint = config.getTelemetryOtlpEndpoint(); const otlpEndpoint = config.getTelemetryOtlpEndpoint();

View File

@ -136,6 +136,7 @@ const baseConfigParams: ConfigParameters = {
userMemory: '', userMemory: '',
geminiMdFileCount: 0, geminiMdFileCount: 0,
approvalMode: ApprovalMode.DEFAULT, approvalMode: ApprovalMode.DEFAULT,
sessionId: 'test-session-id',
}; };
describe('ToolRegistry', () => { describe('ToolRegistry', () => {

View File

@ -71,6 +71,7 @@ describe('checkNextSpeaker', () => {
chatInstance = new GeminiChat( chatInstance = new GeminiChat(
mockModelsInstance, // This is the instance returned by mockGoogleGenAIInstance.getGenerativeModel mockModelsInstance, // This is the instance returned by mockGoogleGenAIInstance.getGenerativeModel
'gemini-pro', // model name 'gemini-pro', // model name
'test-session-id',
{}, {},
[], // initial history [], // initial history
); );

View File

@ -0,0 +1,9 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { randomUUID } from 'crypto';
export const sessionId = randomUUID();