// ============================================================================
// GLOBAL CONTEXT OVERFLOW PREVENTION UTILITIES
// ============================================================================
// Global configuration for context overflow prevention
export interface GlobalContextOverflowConfig {
maxResponseSize: number;
enableTruncation: boolean;
maxSummaryLength: number;
warningThreshold: number; // Percentage of max size to trigger warnings
}
// Load configuration from environment variables
function loadContextOverflowConfig(): Required<GlobalContextOverflowConfig> {
const env = process.env;
return {
maxResponseSize: parseInt(env.SKYENET_CONTEXT_MAX_RESPONSE_SIZE || '50000', 10),
enableTruncation: env.SKYENET_CONTEXT_ENABLE_TRUNCATION !== 'false',
maxSummaryLength: parseInt(env.SKYENET_CONTEXT_MAX_SUMMARY_LENGTH || '1000', 10),
warningThreshold: parseFloat(env.SKYENET_CONTEXT_WARNING_THRESHOLD || '0.8'),
};
}
// Default global configuration (fallback values)
export const DEFAULT_GLOBAL_CONFIG: Required<GlobalContextOverflowConfig> = {
maxResponseSize: 75000, // 75KB response limit (increased for better functionality)
enableTruncation: true,
maxSummaryLength: 2000, // 2KB summary limit (increased for more context)
warningThreshold: 0.85, // Warn at 85% of max size
};
// Response monitoring result
export interface ResponseMonitoringResult {
originalSize: number;
finalSize: number;
truncated: boolean;
warning: boolean;
summary?: string;
}
/**
* Global context overflow prevention utility
* Applies to all MCP tool responses to prevent context window overflow
*/
export class GlobalContextOverflowPrevention {
private config: Required<GlobalContextOverflowConfig>;
constructor(config: Partial<GlobalContextOverflowConfig> = {}) {
// Load from environment variables first, then apply any overrides
const envConfig = loadContextOverflowConfig();
this.config = { ...envConfig, ...config };
}
/**
* Monitor and truncate response if necessary
*
* @param response - The response object to monitor
* @param toolName - Name of the tool for logging
* @returns Monitoring result with truncation applied if needed
*/
monitorResponse(response: any, toolName: string, responseMode?: string): { response: any; monitoring: ResponseMonitoringResult } {
const responseString = JSON.stringify(response);
const originalSize = responseString.length;
const monitoring: ResponseMonitoringResult = {
originalSize,
finalSize: originalSize,
truncated: false,
warning: false,
};
// Apply response mode transformations first (always apply minimal mode regardless of size)
let processedResponse = response;
if (responseMode === 'minimal') {
processedResponse = this.applyMinimalMode(response, toolName);
const processedString = JSON.stringify(processedResponse);
monitoring.finalSize = processedString.length;
monitoring.truncated = originalSize > processedString.length;
}
// Check if response exceeds size limits
const currentSize = JSON.stringify(processedResponse).length;
if (currentSize > this.config.maxResponseSize && this.config.enableTruncation) {
const truncatedResponse = this.truncateResponse(processedResponse, toolName);
const truncatedString = JSON.stringify(truncatedResponse);
monitoring.finalSize = truncatedString.length;
monitoring.truncated = true;
monitoring.summary = this.createTruncationSummary(originalSize, monitoring.finalSize, toolName);
console.warn(`Context overflow prevention: Truncated ${toolName} response from ${originalSize} to ${monitoring.finalSize} characters`);
return { response: truncatedResponse, monitoring };
}
// Check for warning threshold
if (currentSize > this.config.maxResponseSize * this.config.warningThreshold) {
monitoring.warning = true;
console.warn(`Context overflow warning: ${toolName} response size (${currentSize}) approaching limit`);
}
return { response: processedResponse, monitoring };
}
/**
* Truncate response intelligently based on type
*
* @param response - Response to truncate
* @param toolName - Name of the tool
* @returns Truncated response
*/
private truncateResponse(response: any, toolName: string): any {
// Handle different response types
if (response.success && response.data && Array.isArray(response.data.records)) {
// Table operation response - truncate records
return this.truncateTableResponse(response);
} else if (response.success && response.output) {
// Background script response - truncate output
return this.truncateScriptResponse(response);
} else {
// Generic response - truncate string content
return this.truncateGenericResponse(response);
}
}
/**
* Truncate table operation response
*/
private truncateTableResponse(response: any): any {
const maxRecords = Math.max(1, Math.floor(this.config.maxResponseSize / 1000)); // Rough estimate
const truncatedRecords = response.data.records.slice(0, maxRecords);
return {
...response,
data: {
...response.data,
records: truncatedRecords,
count: truncatedRecords.length,
truncated: true,
original_count: response.data.records.length,
summary: {
total_records: response.data.records.length,
displayed_records: truncatedRecords.length,
truncated: true,
message: `Response truncated due to size limits. Showing ${truncatedRecords.length} of ${response.data.records.length} records. Use pagination or field filtering to reduce response size.`
}
},
metadata: {
...response.metadata,
contextOverflowPrevention: true,
responseSize: JSON.stringify(response).length,
truncated: true
}
};
}
/**
* Truncate background script response
*/
private truncateScriptResponse(response: any): any {
const maxOutputLength = this.config.maxSummaryLength;
// Ensure text is always a string, never null - prevents bare null serialization
const originalText = (response.output && response.output.text != null && typeof response.output.text === 'string')
? response.output.text
: '';
const originalHtml = (response.output && response.output.html != null && typeof response.output.html === 'string')
? response.output.html
: '';
const truncatedText = originalText.length > maxOutputLength
? originalText.substring(0, maxOutputLength) + '... [TRUNCATED]'
: originalText;
const truncatedHtml = originalHtml.length > maxOutputLength
? originalHtml.substring(0, maxOutputLength) + '... [TRUNCATED]'
: originalHtml;
return {
...response,
output: {
html: truncatedHtml,
text: truncatedText,
truncated: originalText.length > maxOutputLength,
original_size: originalText.length
},
metadata: {
...response.metadata,
contextOverflowPrevention: true,
responseSize: JSON.stringify(response).length,
truncated: originalText.length > maxOutputLength
}
};
}
/**
* Truncate generic response
*/
private truncateGenericResponse(response: any): any {
const responseString = JSON.stringify(response);
const truncatedString = responseString.substring(0, this.config.maxResponseSize) + '... [TRUNCATED]';
try {
return JSON.parse(truncatedString);
} catch {
// If parsing fails, return a safe truncated response
return {
success: false,
error: {
code: 'RESPONSE_TOO_LARGE',
message: 'Response truncated due to size limits',
details: 'Response exceeded maximum size limit and could not be safely truncated'
},
metadata: {
contextOverflowPrevention: true,
truncated: true,
originalSize: responseString.length
}
};
}
}
/**
* Apply minimal mode transformations to reduce response size
*
* @param response - Response to process
* @param toolName - Name of the tool
* @returns Processed response with minimal mode applied
*/
private applyMinimalMode(response: any, toolName: string): any {
if (!response.success) {
return response; // Don't modify error responses
}
// Handle different response types
if (response.data && Array.isArray(response.data.records)) {
// Table operation response
return this.applyMinimalModeToTableResponse(response);
} else if (response.output) {
// Background script response
return this.applyMinimalModeToScriptResponse(response);
} else if (response.data && response.data.xml_records) {
// Update set response
return this.applyMinimalModeToUpdateSetResponse(response);
}
return response;
}
/**
* Apply minimal mode to table operation response
*/
private applyMinimalModeToTableResponse(response: any): any {
const records = response.data.records || [];
const largeFields = ['script', 'html', 'css', 'description', 'work_notes', 'comments', 'close_notes', 'resolution_notes'];
const processedRecords = records.map((record: any) => {
const cleaned: any = {};
Object.keys(record).forEach(key => {
if (largeFields.includes(key) && typeof record[key] === 'string' && record[key].length > 100) {
cleaned[key] = record[key].substring(0, 100) + '...[truncated]';
} else {
cleaned[key] = record[key];
}
});
return cleaned;
});
// Limit to first 5 records if more than 5
const limitedRecords = processedRecords.length > 5
? processedRecords.slice(0, 5)
: processedRecords;
return {
...response,
data: {
...response.data,
records: limitedRecords,
count: limitedRecords.length,
summary: records.length > 5
? `Showing 5 of ${records.length} records. Use pagination for more.`
: undefined
}
};
}
/**
* Apply minimal mode to background script response
*/
private applyMinimalModeToScriptResponse(response: any): any {
// Ensure output.text is always a string, never null
// This prevents bare null from being serialized in JSON responses
const safeText = (response.output && response.output.text != null && typeof response.output.text === 'string')
? response.output.text
: '';
return {
...response,
output: {
text: safeText,
// Remove HTML in minimal mode
},
metadata: {
...response.metadata,
htmlSize: 0
}
};
}
/**
* Apply minimal mode to update set response
*/
private applyMinimalModeToUpdateSetResponse(response: any): any {
// Handle update set contents with by_type/by_table structure
if (response.data.by_type && response.data.by_table) {
// For contents operation, return simplified structure
const allRecords: any[] = [];
// Flatten all records from by_type structure
Object.values(response.data.by_type).forEach((group: any) => {
if (group.records) {
allRecords.push(...group.records);
}
});
const limitedRecords = allRecords.length > 5 ? allRecords.slice(0, 5) : allRecords;
return {
...response,
data: {
update_set_sys_id: response.data.update_set_sys_id,
update_set_name: response.data.update_set_name,
total_count: response.data.total_count,
records: limitedRecords, // Simple flat array
summary: allRecords.length > 5
? `Showing 5 of ${allRecords.length} records. Use pagination for more.`
: undefined
}
};
}
// Handle recent operation with records
if (response.data.records) {
const records = response.data.records;
const limitedRecords = records.length > 5 ? records.slice(0, 5) : records;
return {
...response,
data: {
...response.data,
records: limitedRecords,
summary: records.length > 5
? `5 of ${records.length} records`
: undefined
}
};
}
return response;
}
/**
* Create truncation summary message
*/
private createTruncationSummary(originalSize: number, finalSize: number, toolName: string): string {
const reductionPercent = Math.round(((originalSize - finalSize) / originalSize) * 100);
return `Context overflow prevention applied to ${toolName}: reduced response size by ${reductionPercent}% (${originalSize} → ${finalSize} characters)`;
}
/**
* Update configuration
*/
updateConfig(newConfig: Partial<GlobalContextOverflowConfig>): void {
this.config = { ...this.config, ...newConfig };
}
/**
* Get current configuration
*/
getConfig(): Required<GlobalContextOverflowConfig> {
return { ...this.config };
}
}
// Global instance for use across all tools
export const globalContextOverflowPrevention = new GlobalContextOverflowPrevention();