route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
298 lines • 12.4 kB
JavaScript
;
/**
* Conversation Queue Manager for OpenAI Provider
* Implements Anthropic-compliant concurrency control for same-session requests
*
* Key Requirements:
* 1. Same session + same conversationID requests must be processed sequentially
* 2. RequestID must have clear numeric ordering within conversation
* 3. Input blocks until previous request completes (finish reason received)
* 4. All finish reasons must be correctly returned (no silent failures)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConversationQueueManager = void 0;
exports.getConversationQueueManager = getConversationQueueManager;
const logger_1 = require("@/utils/logger");
const events_1 = require("events");
/**
* Manages sequential processing of requests within the same conversation
* Ensures Anthropic-compliant ordering and finish reason handling
*/
class ConversationQueueManager extends events_1.EventEmitter {
port;
conversationQueues = new Map();
processingRequests = new Map();
sequenceCounters = new Map();
completedRequests = new Map();
requestStartTimes = new Map();
constructor(port) {
super();
this.port = port;
// Cleanup expired conversations every 5 minutes
setInterval(() => this.cleanupExpiredConversations(), 5 * 60 * 1000);
logger_1.logger.info('ConversationQueueManager initialized', {
port: this.port,
anthropicCompliant: true,
sequentialProcessing: true
});
}
/**
* Enqueue a request for sequential processing within its conversation
* Returns a promise that resolves when the request can be processed
*/
async enqueueRequest(sessionId, conversationId, isStreaming = false) {
const conversationKey = `${sessionId}:${conversationId}`;
// Generate sequential requestId with numeric ordering
const sequenceNumber = this.getNextSequenceNumber(conversationKey);
const requestId = `${conversationKey}:seq${sequenceNumber.toString().padStart(4, '0')}:${Date.now()}`;
return new Promise((resolve, reject) => {
const request = {
requestId,
sessionId,
conversationId,
sequenceNumber,
timestamp: new Date(),
isStreaming,
resolve,
reject
};
// Add to conversation queue
if (!this.conversationQueues.has(conversationKey)) {
this.conversationQueues.set(conversationKey, []);
}
const queue = this.conversationQueues.get(conversationKey);
queue.push(request);
logger_1.logger.debug('Request enqueued for sequential processing', {
requestId,
sessionId,
conversationId,
sequenceNumber,
queuePosition: queue.length,
isStreaming,
conversationKey
});
// Try to process immediately if no request is currently processing
this.processNextInQueue(conversationKey).catch(error => {
logger_1.logger.error('Error processing next in queue', { conversationKey, error });
});
});
}
/**
* Mark a request as completed and process next in queue
* Must be called when finish reason is received
*/
completeRequest(requestId, finishReason) {
const processingRequest = this.processingRequests.get(requestId);
if (!processingRequest) {
logger_1.logger.warn('Attempted to complete non-processing request', {
requestId,
finishReason
});
return;
}
const conversationKey = `${processingRequest.sessionId}:${processingRequest.conversationId}`;
const startTime = this.requestStartTimes.get(requestId);
const processingTime = startTime ? Date.now() - startTime.getTime() : 0;
// Remove from processing
this.processingRequests.delete(requestId);
this.requestStartTimes.delete(requestId);
// Update completion stats
const completed = this.completedRequests.get(conversationKey) || 0;
this.completedRequests.set(conversationKey, completed + 1);
logger_1.logger.info('Request completed, processing next in queue', {
requestId,
sessionId: processingRequest.sessionId,
conversationId: processingRequest.conversationId,
sequenceNumber: processingRequest.sequenceNumber,
finishReason,
processingTimeMs: processingTime,
completedInConversation: completed + 1
});
// Emit completion event
this.emit('requestCompleted', {
requestId,
conversationKey,
finishReason,
processingTime
});
// Process next request in the same conversation
this.processNextInQueue(conversationKey).catch(error => {
logger_1.logger.error('Error processing next in queue after completion', { conversationKey, error });
});
}
/**
* Handle request failure and process next in queue
*/
failRequest(requestId, error) {
const processingRequest = this.processingRequests.get(requestId);
if (!processingRequest) {
logger_1.logger.warn('Attempted to fail non-processing request', {
requestId,
error: error instanceof Error ? error.message : String(error)
});
return;
}
const conversationKey = `${processingRequest.sessionId}:${processingRequest.conversationId}`;
const startTime = this.requestStartTimes.get(requestId);
const processingTime = startTime ? Date.now() - startTime.getTime() : 0;
// Remove from processing
this.processingRequests.delete(requestId);
this.requestStartTimes.delete(requestId);
logger_1.logger.error('Request failed, processing next in queue', {
requestId,
sessionId: processingRequest.sessionId,
conversationId: processingRequest.conversationId,
sequenceNumber: processingRequest.sequenceNumber,
error: error instanceof Error ? error.message : String(error),
processingTimeMs: processingTime
});
// Emit failure event
this.emit('requestFailed', {
requestId,
conversationKey,
error,
processingTime
});
// Process next request in the same conversation
this.processNextInQueue(conversationKey).catch(error => {
logger_1.logger.error('Error processing next in queue after failure', { conversationKey, error });
});
}
/**
* Get queue statistics
*/
getQueueStats() {
let totalPendingRequests = 0;
let longestQueue = 0;
let totalWaitTime = 0;
let waitingRequests = 0;
let totalCompleted = 0;
const now = new Date();
for (const queue of this.conversationQueues.values()) {
totalPendingRequests += queue.length;
longestQueue = Math.max(longestQueue, queue.length);
// Calculate wait times for pending requests
for (const request of queue) {
const waitTime = now.getTime() - request.timestamp.getTime();
totalWaitTime += waitTime;
waitingRequests++;
}
}
for (const completed of this.completedRequests.values()) {
totalCompleted += completed;
}
return {
totalQueues: this.conversationQueues.size,
totalPendingRequests,
averageWaitTime: waitingRequests > 0 ? Math.round(totalWaitTime / waitingRequests) : 0,
longestQueue,
completedRequests: totalCompleted
};
}
/**
* Process the next request in a conversation queue
*/
async processNextInQueue(conversationKey) {
const queue = this.conversationQueues.get(conversationKey);
if (!queue || queue.length === 0) {
return;
}
// Check if already processing a request for this conversation
const isAlreadyProcessing = Array.from(this.processingRequests.values())
.some(req => `${req.sessionId}:${req.conversationId}` === conversationKey);
if (isAlreadyProcessing) {
return;
}
// Get next request from queue
const nextRequest = queue.shift();
this.processingRequests.set(nextRequest.requestId, nextRequest);
this.requestStartTimes.set(nextRequest.requestId, new Date());
logger_1.logger.info('Starting sequential request processing', {
requestId: nextRequest.requestId,
sessionId: nextRequest.sessionId,
conversationId: nextRequest.conversationId,
sequenceNumber: nextRequest.sequenceNumber,
remainingInQueue: queue.length,
isStreaming: nextRequest.isStreaming
});
// Resolve the promise to allow request processing
nextRequest.resolve({
requestId: nextRequest.requestId,
sequenceNumber: nextRequest.sequenceNumber
});
// Emit processing start event
this.emit('requestStarted', {
requestId: nextRequest.requestId,
conversationKey,
sequenceNumber: nextRequest.sequenceNumber
});
}
/**
* Get next sequence number for a conversation
*/
getNextSequenceNumber(conversationKey) {
const current = this.sequenceCounters.get(conversationKey) || 0;
const next = current + 1;
this.sequenceCounters.set(conversationKey, next);
return next;
}
/**
* Clean up expired conversations and their queues
*/
cleanupExpiredConversations() {
const now = new Date();
const expiredThreshold = 2 * 60 * 60 * 1000; // 2 hours
const expiredConversations = [];
// Find expired conversations
for (const [conversationKey, queue] of this.conversationQueues.entries()) {
if (queue.length === 0) {
// Check if any recent activity
const hasRecentProcessing = Array.from(this.processingRequests.values())
.some(req => `${req.sessionId}:${req.conversationId}` === conversationKey);
if (!hasRecentProcessing) {
expiredConversations.push(conversationKey);
}
}
else {
// Check if oldest request is too old
const oldestRequest = queue[0];
const age = now.getTime() - oldestRequest.timestamp.getTime();
if (age > expiredThreshold) {
expiredConversations.push(conversationKey);
}
}
}
// Clean up expired conversations
for (const conversationKey of expiredConversations) {
const queue = this.conversationQueues.get(conversationKey);
if (queue) {
// Reject all pending requests
for (const request of queue) {
request.reject(new Error('Conversation expired'));
}
}
this.conversationQueues.delete(conversationKey);
this.sequenceCounters.delete(conversationKey);
this.completedRequests.delete(conversationKey);
}
if (expiredConversations.length > 0) {
logger_1.logger.info('Cleaned up expired conversations', {
expiredCount: expiredConversations.length,
remainingQueues: this.conversationQueues.size
});
}
}
}
exports.ConversationQueueManager = ConversationQueueManager;
// Global conversation queue manager instances per port
const queueManagers = new Map();
/**
* Get or create conversation queue manager for a specific port
*/
function getConversationQueueManager(port) {
if (!queueManagers.has(port)) {
queueManagers.set(port, new ConversationQueueManager(port));
}
return queueManagers.get(port);
}
//# sourceMappingURL=conversation-queue-manager.js.map