mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Session-Level Conversation History Management (#1113)
This commit is contained in:
@@ -142,6 +142,7 @@ describe('ClearcutLogger', () => {
|
||||
|
||||
const loggerConfig = makeFakeConfig({
|
||||
...config,
|
||||
sessionId: 'test-session-id',
|
||||
});
|
||||
ClearcutLogger.clearInstance();
|
||||
|
||||
@@ -248,7 +249,7 @@ describe('ClearcutLogger', () => {
|
||||
|
||||
it('logs default metadata', () => {
|
||||
// Define expected values
|
||||
const session_id = 'my-session-id';
|
||||
const session_id = 'test-session-id';
|
||||
const auth_type = AuthType.USE_GEMINI;
|
||||
const google_accounts = 123;
|
||||
const surface = 'ide-1234';
|
||||
@@ -260,7 +261,7 @@ describe('ClearcutLogger', () => {
|
||||
// Setup logger with expected values
|
||||
const { logger, loggerConfig } = setup({
|
||||
lifetimeGoogleAccounts: google_accounts,
|
||||
config: { sessionId: session_id },
|
||||
config: {},
|
||||
});
|
||||
vi.spyOn(loggerConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: auth_type,
|
||||
|
||||
@@ -25,7 +25,7 @@ export {
|
||||
parseTelemetryTargetValue,
|
||||
} from './config.js';
|
||||
export {
|
||||
logCliConfiguration,
|
||||
logStartSession,
|
||||
logUserPrompt,
|
||||
logToolCall,
|
||||
logApiRequest,
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
import {
|
||||
logApiRequest,
|
||||
logApiResponse,
|
||||
logCliConfiguration,
|
||||
logStartSession,
|
||||
logUserPrompt,
|
||||
logToolCall,
|
||||
logFlashFallback,
|
||||
@@ -116,7 +116,7 @@ describe('loggers', () => {
|
||||
});
|
||||
|
||||
it('logs the chat compression event to QwenLogger', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const mockConfig = makeFakeConfig({ sessionId: 'test-session-id' });
|
||||
|
||||
const event = makeChatCompressionEvent({
|
||||
tokens_before: 9001,
|
||||
@@ -131,7 +131,7 @@ describe('loggers', () => {
|
||||
});
|
||||
|
||||
it('records the chat compression event to OTEL', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const mockConfig = makeFakeConfig({ sessionId: 'test-session-id' });
|
||||
|
||||
logChatCompression(
|
||||
mockConfig,
|
||||
@@ -177,10 +177,12 @@ describe('loggers', () => {
|
||||
getTargetDir: () => 'target-dir',
|
||||
getProxy: () => 'http://test.proxy.com:8080',
|
||||
getOutputFormat: () => OutputFormat.JSON,
|
||||
getToolRegistry: () => undefined,
|
||||
getChatRecordingService: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const startSessionEvent = new StartSessionEvent(mockConfig);
|
||||
logCliConfiguration(mockConfig, startSessionEvent);
|
||||
logStartSession(mockConfig, startSessionEvent);
|
||||
|
||||
expect(mockLogger.emit).toHaveBeenCalledWith({
|
||||
body: 'CLI configuration loaded.',
|
||||
@@ -281,7 +283,8 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
} as Config;
|
||||
getChatRecordingService: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
recordApiResponseMetrics: vi.fn(),
|
||||
@@ -368,7 +371,7 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
} as Config;
|
||||
} as unknown as Config;
|
||||
|
||||
it('should log an API request with request_text', () => {
|
||||
const event = new ApiRequestEvent(
|
||||
@@ -498,6 +501,7 @@ describe('loggers', () => {
|
||||
const cfg2 = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTargetDir: () => 'target-dir',
|
||||
getProjectRoot: () => '/test/project/root',
|
||||
getProxy: () => 'http://test.proxy.com:8080',
|
||||
getContentGeneratorConfig: () =>
|
||||
({ model: 'test-model' }) as ContentGeneratorConfig,
|
||||
@@ -530,7 +534,8 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
} as Config;
|
||||
getChatRecordingService: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
recordToolCallMetrics: vi.fn(),
|
||||
@@ -1029,7 +1034,7 @@ describe('loggers', () => {
|
||||
});
|
||||
|
||||
it('logs the event to Clearcut and OTEL', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const mockConfig = makeFakeConfig({ sessionId: 'test-session-id' });
|
||||
const event = new MalformedJsonResponseEvent('test-model');
|
||||
|
||||
logMalformedJsonResponse(mockConfig, event);
|
||||
|
||||
@@ -101,7 +101,7 @@ function getCommonAttributes(config: Config): LogAttributes {
|
||||
};
|
||||
}
|
||||
|
||||
export function logCliConfiguration(
|
||||
export function logStartSession(
|
||||
config: Config,
|
||||
event: StartSessionEvent,
|
||||
): void {
|
||||
@@ -172,6 +172,7 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
config.getChatRecordingService()?.recordUiTelemetryEvent(uiEvent);
|
||||
QwenLogger.getInstance(config)?.logToolCallEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
@@ -339,6 +340,7 @@ export function logApiError(config: Config, event: ApiErrorEvent): void {
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
config.getChatRecordingService()?.recordUiTelemetryEvent(uiEvent);
|
||||
QwenLogger.getInstance(config)?.logApiErrorEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
@@ -405,6 +407,7 @@ export function logApiResponse(config: Config, event: ApiResponseEvent): void {
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
config.getChatRecordingService()?.recordUiTelemetryEvent(uiEvent);
|
||||
QwenLogger.getInstance(config)?.logApiResponseEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
const attributes: LogAttributes = {
|
||||
|
||||
@@ -134,7 +134,9 @@ describe('Telemetry Metrics', () => {
|
||||
});
|
||||
|
||||
it('records token compression with the correct attributes', () => {
|
||||
const config = makeFakeConfig({});
|
||||
const config = makeFakeConfig({
|
||||
sessionId: 'test-session-id',
|
||||
});
|
||||
initializeMetricsModule(config);
|
||||
|
||||
recordChatCompressionMetricsModule(config, {
|
||||
|
||||
@@ -59,6 +59,7 @@ const makeFakeConfig = (overrides: Partial<Config> = {}): Config => {
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
getOutputFormat: () => 'text',
|
||||
getToolRegistry: () => undefined,
|
||||
...overrides,
|
||||
};
|
||||
return defaults as Config;
|
||||
|
||||
@@ -39,8 +39,8 @@ import type {
|
||||
ExtensionDisableEvent,
|
||||
AuthEvent,
|
||||
RipgrepFallbackEvent,
|
||||
EndSessionEvent,
|
||||
} from '../types.js';
|
||||
import { EndSessionEvent } from '../types.js';
|
||||
import type {
|
||||
RumEvent,
|
||||
RumViewEvent,
|
||||
@@ -102,6 +102,7 @@ export class QwenLogger {
|
||||
private lastFlushTime: number = Date.now();
|
||||
|
||||
private userId: string;
|
||||
|
||||
private sessionId: string;
|
||||
|
||||
/**
|
||||
@@ -115,17 +116,12 @@ export class QwenLogger {
|
||||
*/
|
||||
private pendingFlush: boolean = false;
|
||||
|
||||
private isShutdown: boolean = false;
|
||||
|
||||
private constructor(config?: Config) {
|
||||
private constructor(config: Config) {
|
||||
this.config = config;
|
||||
this.events = new FixedDeque<RumEvent>(Array, MAX_EVENTS);
|
||||
this.installationManager = new InstallationManager();
|
||||
this.userId = this.generateUserId();
|
||||
this.sessionId =
|
||||
typeof this.config?.getSessionId === 'function'
|
||||
? this.config.getSessionId()
|
||||
: '';
|
||||
this.sessionId = config.getSessionId();
|
||||
}
|
||||
|
||||
private generateUserId(): string {
|
||||
@@ -139,10 +135,6 @@ export class QwenLogger {
|
||||
return undefined;
|
||||
if (!QwenLogger.instance) {
|
||||
QwenLogger.instance = new QwenLogger(config);
|
||||
process.on(
|
||||
'exit',
|
||||
QwenLogger.instance.shutdown.bind(QwenLogger.instance),
|
||||
);
|
||||
}
|
||||
|
||||
return QwenLogger.instance;
|
||||
@@ -241,10 +233,10 @@ export class QwenLogger {
|
||||
id: this.userId,
|
||||
},
|
||||
session: {
|
||||
id: this.sessionId,
|
||||
id: this.sessionId || this.config?.getSessionId(),
|
||||
},
|
||||
view: {
|
||||
id: this.sessionId,
|
||||
id: this.sessionId || this.config?.getSessionId(),
|
||||
name: 'qwen-code-cli',
|
||||
},
|
||||
os: osMetadata,
|
||||
@@ -364,7 +356,24 @@ export class QwenLogger {
|
||||
}
|
||||
|
||||
// session events
|
||||
logStartSessionEvent(event: StartSessionEvent): void {
|
||||
async logStartSessionEvent(event: StartSessionEvent): Promise<void> {
|
||||
// Flush all pending events with the old session ID first.
|
||||
// If flush fails, discard the pending events to avoid mixing sessions.
|
||||
await this.flushToRum().catch((error: unknown) => {
|
||||
if (this.config?.getDebugMode()) {
|
||||
console.debug(
|
||||
'Error flushing pending events before session start:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear any remaining events (discard if flush failed)
|
||||
this.events.clear();
|
||||
|
||||
// Now set the new session ID
|
||||
this.sessionId = event.session_id;
|
||||
|
||||
const applicationEvent = this.createViewEvent('session', 'session_start', {
|
||||
properties: {
|
||||
model: event.model,
|
||||
@@ -852,14 +861,6 @@ export class QwenLogger {
|
||||
}
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
if (this.isShutdown) return;
|
||||
|
||||
this.isShutdown = true;
|
||||
const event = new EndSessionEvent(this.config);
|
||||
this.logEndSessionEvent(event);
|
||||
}
|
||||
|
||||
private requeueFailedEvents(eventsToSend: RumEvent[]): void {
|
||||
// Add the events back to the front of the queue to be retried, but limit retry queue size
|
||||
const eventsToRetry = eventsToSend.slice(-MAX_RETRY_EVENTS); // Keep only the most recent events
|
||||
|
||||
@@ -24,7 +24,6 @@ describe('telemetry', () => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
mockConfig = new Config({
|
||||
sessionId: 'test-session-id',
|
||||
model: 'test-model',
|
||||
targetDir: '/test/dir',
|
||||
debugMode: false,
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from './tool-call-decision.js';
|
||||
import type { FileOperation } from './metrics.js';
|
||||
export { ToolCallDecision };
|
||||
import type { ToolRegistry } from '../tools/tool-registry.js';
|
||||
import type { OutputFormat } from '../output/types.js';
|
||||
|
||||
export interface BaseTelemetryEvent {
|
||||
@@ -31,6 +30,7 @@ type CommonFields = keyof BaseTelemetryEvent;
|
||||
export class StartSessionEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'cli_config';
|
||||
'event.timestamp': string;
|
||||
session_id: string;
|
||||
model: string;
|
||||
embedding_model: string;
|
||||
sandbox_enabled: boolean;
|
||||
@@ -48,9 +48,10 @@ export class StartSessionEvent implements BaseTelemetryEvent {
|
||||
mcp_tools?: string;
|
||||
output_format: OutputFormat;
|
||||
|
||||
constructor(config: Config, toolRegistry?: ToolRegistry) {
|
||||
constructor(config: Config) {
|
||||
const generatorConfig = config.getContentGeneratorConfig();
|
||||
const mcpServers = config.getMcpServers();
|
||||
const toolRegistry = config.getToolRegistry();
|
||||
|
||||
let useGemini = false;
|
||||
let useVertex = false;
|
||||
@@ -60,6 +61,7 @@ export class StartSessionEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
|
||||
this['event.name'] = 'cli_config';
|
||||
this.session_id = config.getSessionId();
|
||||
this.model = config.getModel();
|
||||
this.embedding_model = config.getEmbeddingModel();
|
||||
this.sandbox_enabled =
|
||||
|
||||
@@ -152,6 +152,18 @@ export class UiTelemetryService extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets metrics to the initial state (used when resuming a session).
|
||||
*/
|
||||
reset(): void {
|
||||
this.#metrics = createInitialMetrics();
|
||||
this.#lastPromptTokenCount = 0;
|
||||
this.emit('update', {
|
||||
metrics: this.#metrics,
|
||||
lastPromptTokenCount: this.#lastPromptTokenCount,
|
||||
});
|
||||
}
|
||||
|
||||
private getOrCreateModelMetrics(modelName: string): ModelMetrics {
|
||||
if (!this.#metrics.models[modelName]) {
|
||||
this.#metrics.models[modelName] = createInitialModelMetrics();
|
||||
|
||||
Reference in New Issue
Block a user