cost-claude
Version:
Claude Code cost monitoring, analytics, and optimization toolkit
353 lines • 15.5 kB
JavaScript
import { EventEmitter } from 'events';
import { ProjectParser } from '../core/project-parser.js';
import { JSONLParser } from '../core/jsonl-parser.js';
import { CostCalculator } from '../core/cost-calculator.js';
import { logger } from '../utils/logger.js';
export class SessionDetector extends EventEmitter {
jsonlParser;
costCalculator;
sessions = new Map();
config;
constructor(config = {}) {
super();
this.jsonlParser = new JSONLParser();
this.costCalculator = new CostCalculator();
this.costCalculator.ensureRatesLoaded().catch(err => {
logger.error('Failed to load pricing rates:', err);
});
this.config = {
inactivityTimeout: config.inactivityTimeout ?? 300000,
summaryMessageTimeout: config.summaryMessageTimeout ?? 5000,
taskCompletionTimeout: config.taskCompletionTimeout ?? 3000,
delayedTaskCompletionTimeout: config.delayedTaskCompletionTimeout ?? 30000,
minTaskCost: config.minTaskCost ?? 0.01,
minTaskMessages: config.minTaskMessages ?? 1,
enableProgressNotifications: config.enableProgressNotifications ?? true,
progressCheckInterval: config.progressCheckInterval ?? 10000,
minProgressCost: config.minProgressCost ?? 0.02,
minProgressDuration: config.minProgressDuration ?? 15000,
};
}
processMessage(message, filePath) {
const sessionId = message.sessionId || 'unknown';
if (!this.sessions.has(sessionId)) {
const projectName = ProjectParser.getProjectFromMessage(message, filePath);
this.sessions.set(sessionId, {
messages: [],
totalCost: 0,
startTime: new Date(message.timestamp),
lastActivity: new Date(message.timestamp),
projectName,
hasSummary: false,
filePath,
currentTaskCost: 0,
currentTaskAssistantCount: 0,
taskInProgress: false
});
logger.debug(`New session detected: ${sessionId} for project: ${projectName}`);
}
const session = this.sessions.get(sessionId);
session.messages.push(message);
session.lastActivity = new Date(message.timestamp);
session.lastMessageUuid = message.uuid;
if (session.inactivityTimer) {
clearTimeout(session.inactivityTimer);
}
if (session.summaryTimer) {
clearTimeout(session.summaryTimer);
}
if (session.taskTimer) {
clearTimeout(session.taskTimer);
}
if (session.delayedTaskTimer) {
clearTimeout(session.delayedTaskTimer);
}
if (session.progressTimer) {
clearTimeout(session.progressTimer);
}
if (message.type === 'user') {
if (session.currentTaskAssistantCount > 0) {
logger.debug(`Task interrupted by new user message in session ${sessionId}`);
}
session.currentTaskStartTime = new Date(message.timestamp);
session.currentTaskCost = 0;
session.currentTaskAssistantCount = 0;
session.lastAssistantMessageTime = undefined;
session.taskInProgress = false;
session.lastProgressNotificationTime = undefined;
}
else if (message.type === 'assistant') {
let messageCost = 0;
if (message.costUSD !== null && message.costUSD !== undefined) {
messageCost = message.costUSD;
}
else {
const content = this.jsonlParser.parseMessageContent(message);
if (content?.usage) {
messageCost = this.costCalculator.calculate(content.usage);
}
}
session.totalCost += messageCost;
if (!session.currentTaskStartTime) {
session.currentTaskStartTime = new Date(message.timestamp);
}
session.currentTaskCost += messageCost;
session.currentTaskAssistantCount++;
session.lastAssistantMessageTime = new Date(message.timestamp);
session.taskInProgress = true;
session.taskTimer = setTimeout(() => {
this.completeTask(sessionId, 'immediate');
}, this.config.taskCompletionTimeout);
session.delayedTaskTimer = setTimeout(() => {
this.completeTask(sessionId, 'delayed');
}, this.config.delayedTaskCompletionTimeout);
if (this.config.enableProgressNotifications && !session.progressTimer) {
this.startProgressMonitoring(sessionId);
}
logger.debug(`Assistant message in session ${sessionId}, immediate timer: ${this.config.taskCompletionTimeout}ms, delayed timer: ${this.config.delayedTaskCompletionTimeout}ms`);
}
else if (message.type === 'summary') {
session.hasSummary = true;
session.summaryText = message.summary || 'Task completed';
logger.debug(`Summary detected for session ${sessionId}: ${session.summaryText}`);
session.summaryTimer = setTimeout(() => {
this.completeSession(sessionId, 'summary');
}, this.config.summaryMessageTimeout);
return;
}
session.inactivityTimer = setTimeout(() => {
this.completeSession(sessionId, 'inactivity');
}, this.config.inactivityTimeout);
}
startProgressMonitoring(sessionId) {
const session = this.sessions.get(sessionId);
if (!session)
return;
if (session.progressTimer) {
clearTimeout(session.progressTimer);
}
session.progressTimer = setInterval(() => {
this.checkTaskProgress(sessionId);
}, this.config.progressCheckInterval);
}
checkTaskProgress(sessionId) {
const session = this.sessions.get(sessionId);
if (!session || !session.taskInProgress) {
if (session?.progressTimer) {
clearInterval(session.progressTimer);
session.progressTimer = undefined;
}
return;
}
const now = Date.now();
const taskDuration = session.lastAssistantMessageTime
? now - (session.currentTaskStartTime?.getTime() || 0)
: 0;
if (taskDuration >= this.config.minProgressDuration &&
session.currentTaskCost >= this.config.minProgressCost) {
const timeSinceLastNotification = session.lastProgressNotificationTime
? now - session.lastProgressNotificationTime.getTime()
: Infinity;
if (timeSinceLastNotification >= this.config.progressCheckInterval) {
const progressData = {
sessionId,
projectName: session.projectName,
currentCost: session.currentTaskCost,
currentDuration: taskDuration,
assistantMessageCount: session.currentTaskAssistantCount,
isActive: true,
estimatedCompletion: this.estimateCompletion(session)
};
logger.debug(`Task progress in session ${sessionId}`, {
cost: progressData.currentCost,
duration: progressData.currentDuration,
messages: progressData.assistantMessageCount
});
this.emit('task-progress', progressData);
session.lastProgressNotificationTime = new Date(now);
}
}
}
estimateCompletion(session) {
if (!session.lastAssistantMessageTime)
return undefined;
const timeSinceLastMessage = Date.now() - session.lastAssistantMessageTime.getTime();
const averageMessageInterval = session.currentTaskAssistantCount > 1
? (session.lastAssistantMessageTime.getTime() - session.currentTaskStartTime.getTime()) / session.currentTaskAssistantCount
: 5000;
if (timeSinceLastMessage > averageMessageInterval * 2) {
return Math.min(5000, this.config.taskCompletionTimeout - timeSinceLastMessage);
}
return averageMessageInterval * 2;
}
completeTask(sessionId, completionType = 'immediate') {
const session = this.sessions.get(sessionId);
if (!session || session.currentTaskAssistantCount === 0)
return;
if (session.taskTimer) {
clearTimeout(session.taskTimer);
session.taskTimer = undefined;
}
if (session.delayedTaskTimer) {
clearTimeout(session.delayedTaskTimer);
session.delayedTaskTimer = undefined;
}
if (session.progressTimer) {
clearInterval(session.progressTimer);
session.progressTimer = undefined;
}
if (session.currentTaskCost < this.config.minTaskCost ||
session.currentTaskAssistantCount < this.config.minTaskMessages) {
logger.debug(`Task in session ${sessionId} below thresholds (cost: ${session.currentTaskCost}, messages: ${session.currentTaskAssistantCount})`);
session.currentTaskCost = 0;
session.currentTaskAssistantCount = 0;
session.currentTaskStartTime = undefined;
session.lastAssistantMessageTime = undefined;
session.taskInProgress = false;
session.lastProgressNotificationTime = undefined;
return;
}
const taskDuration = session.lastAssistantMessageTime
? session.lastAssistantMessageTime.getTime() - (session.currentTaskStartTime?.getTime() || 0)
: 0;
const taskCompletionData = {
sessionId,
projectName: session.projectName,
taskCost: session.currentTaskCost,
taskDuration,
assistantMessageCount: session.currentTaskAssistantCount,
lastMessageUuid: session.lastMessageUuid || '',
timestamp: session.lastAssistantMessageTime || new Date(),
completionType
};
logger.debug(`Task completed in session ${sessionId} (${completionType})`, {
cost: taskCompletionData.taskCost,
messages: taskCompletionData.assistantMessageCount,
duration: taskCompletionData.taskDuration,
completionType
});
this.emit('task-completed', taskCompletionData);
session.currentTaskCost = 0;
session.currentTaskAssistantCount = 0;
session.currentTaskStartTime = undefined;
session.lastAssistantMessageTime = undefined;
session.taskInProgress = false;
session.lastProgressNotificationTime = undefined;
}
completeSession(sessionId, reason) {
const session = this.sessions.get(sessionId);
if (!session)
return;
if (session.inactivityTimer) {
clearTimeout(session.inactivityTimer);
}
if (session.summaryTimer) {
clearTimeout(session.summaryTimer);
}
if (session.taskTimer) {
clearTimeout(session.taskTimer);
}
if (session.delayedTaskTimer) {
clearTimeout(session.delayedTaskTimer);
}
if (session.progressTimer) {
clearInterval(session.progressTimer);
}
const duration = session.lastActivity.getTime() - session.startTime.getTime();
const completionData = {
sessionId,
projectName: session.projectName,
summary: session.summaryText || this.generateSummary(session.messages),
totalCost: session.totalCost,
messageCount: session.messages.length,
duration,
startTime: session.startTime,
endTime: session.lastActivity,
lastMessageUuid: session.lastMessageUuid || ''
};
logger.debug(`Session completed (${reason}): ${sessionId}`, {
project: completionData.projectName,
cost: completionData.totalCost,
messages: completionData.messageCount,
duration: completionData.duration
});
this.emit('session-completed', completionData);
this.sessions.delete(sessionId);
}
generateSummary(messages) {
const userMessages = messages.filter(m => m.type === 'user').length;
const assistantMessages = messages.filter(m => m.type === 'assistant').length;
if (userMessages === 0) {
return 'No user interaction';
}
else if (assistantMessages === 0) {
return 'No assistant responses';
}
else {
return `${userMessages} questions, ${assistantMessages} responses`;
}
}
completeAllSessions() {
const sessionIds = Array.from(this.sessions.keys());
sessionIds.forEach(sessionId => {
this.completeSession(sessionId, 'manual');
});
}
getActiveSessions() {
return Array.from(this.sessions.keys());
}
getSessionInfo(sessionId) {
const session = this.sessions.get(sessionId);
if (!session)
return null;
return {
sessionId,
projectName: session.projectName,
totalCost: session.totalCost,
messageCount: session.messages.length,
hasSummary: session.hasSummary,
lastActivity: session.lastActivity,
duration: session.lastActivity.getTime() - session.startTime.getTime()
};
}
isSessionIdle(sessionId) {
const session = this.sessions.get(sessionId);
if (!session)
return false;
const idleTime = Date.now() - session.lastActivity.getTime();
return idleTime > this.config.inactivityTimeout;
}
updateConfig(config) {
if (config.inactivityTimeout !== undefined) {
this.config.inactivityTimeout = config.inactivityTimeout;
}
if (config.summaryMessageTimeout !== undefined) {
this.config.summaryMessageTimeout = config.summaryMessageTimeout;
}
if (config.taskCompletionTimeout !== undefined) {
this.config.taskCompletionTimeout = config.taskCompletionTimeout;
}
if (config.delayedTaskCompletionTimeout !== undefined) {
this.config.delayedTaskCompletionTimeout = config.delayedTaskCompletionTimeout;
}
if (config.minTaskCost !== undefined) {
this.config.minTaskCost = config.minTaskCost;
}
if (config.minTaskMessages !== undefined) {
this.config.minTaskMessages = config.minTaskMessages;
}
if (config.enableProgressNotifications !== undefined) {
this.config.enableProgressNotifications = config.enableProgressNotifications;
}
if (config.progressCheckInterval !== undefined) {
this.config.progressCheckInterval = config.progressCheckInterval;
}
if (config.minProgressCost !== undefined) {
this.config.minProgressCost = config.minProgressCost;
}
if (config.minProgressDuration !== undefined) {
this.config.minProgressDuration = config.minProgressDuration;
}
}
}
//# sourceMappingURL=session-detector.js.map