Enhance error handling and validation in agents and routes

- Added logging for empty responses in GeminiAgent and OpenRouterAgent to track potential session context issues.
- Refactored settings file path usage in OpenRouterAgent to use a constant for better maintainability.
- Improved error handling in SessionRoutes to log generator failures with detailed context.
- Implemented JSON parsing error handling in SettingsRoutes to manage corrupted settings files gracefully.
- Added validation for CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES, CLAUDE_MEM_OPENROUTER_MAX_TOKENS, and CLAUDE_MEM_OPENROUTER_SITE_URL in SettingsRoutes to ensure valid configuration.
This commit is contained in:
Alex Newman
2025-12-26 22:36:14 -05:00
parent 69cd734b53
commit 1fc1419edd
5 changed files with 122 additions and 72 deletions
File diff suppressed because one or more lines are too long
+5
View File
@@ -170,6 +170,11 @@ export class GeminiAgent {
// Process response (no original timestamp for init - not from queue)
await this.processGeminiResponse(session, initResponse.content, worker, tokensUsed, null);
} else {
logger.warn('SDK', 'Empty Gemini init response - session may lack context', {
sessionId: session.sessionDbId,
model
});
}
// Process pending messages
+11 -7
View File
@@ -11,14 +11,13 @@
* - Support dynamic model selection across providers
*/
import path from 'path';
import { homedir } from 'os';
import { DatabaseManager } from './DatabaseManager.js';
import { SessionManager } from './SessionManager.js';
import { logger } from '../../utils/logger.js';
import { parseObservations, parseSummary } from '../../sdk/parser.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
import { ModeManager } from '../domain/ModeManager.js';
@@ -131,6 +130,11 @@ export class OpenRouterAgent {
// Process response (no original timestamp for init - not from queue)
await this.processOpenRouterResponse(session, initResponse.content, worker, tokensUsed, null);
} else {
logger.warn('SDK', 'Empty OpenRouter init response - session may lack context', {
sessionId: session.sessionDbId,
model
});
}
// Process pending messages
@@ -263,7 +267,7 @@ export class OpenRouterAgent {
*/
private truncateHistory(history: ConversationMessage[]): ConversationMessage[] {
const settings = SettingsDefaultsManager.loadFromFile(
path.join(homedir(), '.claude-mem', 'settings.json')
USER_SETTINGS_PATH
);
const MAX_CONTEXT_MESSAGES = parseInt(settings.CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES) || DEFAULT_MAX_CONTEXT_MESSAGES;
@@ -377,7 +381,7 @@ export class OpenRouterAgent {
if (tokensUsed) {
const inputTokens = data.usage?.prompt_tokens || 0;
const outputTokens = data.usage?.completion_tokens || 0;
// Rough cost estimate (Claude 3.5 Sonnet pricing: $3/MTok input, $15/MTok output)
// Token usage (cost varies by model - many OpenRouter models are free)
const estimatedCost = (inputTokens / 1000000 * 3) + (outputTokens / 1000000 * 15);
logger.info('SDK', 'OpenRouter API usage', {
@@ -568,7 +572,7 @@ export class OpenRouterAgent {
* Get OpenRouter configuration from settings or environment
*/
private getOpenRouterConfig(): { apiKey: string; model: string; siteUrl?: string; appName?: string } {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
const settingsPath = USER_SETTINGS_PATH;
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
// API key: check settings first, then environment variable
@@ -589,7 +593,7 @@ export class OpenRouterAgent {
* Check if OpenRouter is available (has API key configured)
*/
export function isOpenRouterAvailable(): boolean {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
const settingsPath = USER_SETTINGS_PATH;
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
return !!(settings.CLAUDE_MEM_OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY);
}
@@ -598,7 +602,7 @@ export function isOpenRouterAvailable(): boolean {
* Check if OpenRouter is the selected provider
*/
export function isOpenRouterSelected(): boolean {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
const settingsPath = USER_SETTINGS_PATH;
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
return settings.CLAUDE_MEM_PROVIDER === 'openrouter';
}
@@ -135,6 +135,13 @@ export class SessionRoutes extends BaseRouteHandler {
session.currentProvider = provider;
session.generatorPromise = agent.startSession(session, this.workerService)
.catch(error => {
logger.error('SESSION', `Generator failed`, {
sessionId: session.sessionDbId,
provider: provider,
error: error.message
}, error);
})
.finally(() => {
logger.info('SESSION', `Generator finished`, { sessionId: session.sessionDbId });
session.generatorPromise = null;
@@ -71,7 +71,16 @@ export class SettingsRoutes extends BaseRouteHandler {
if (existsSync(settingsPath)) {
const settingsData = readFileSync(settingsPath, 'utf-8');
settings = JSON.parse(settingsData);
try {
settings = JSON.parse(settingsData);
} catch (parseError) {
logger.error('SETTINGS', 'Failed to parse settings file', { settingsPath }, parseError as Error);
res.status(500).json({
success: false,
error: 'Settings file is corrupted. Delete ~/.claude-mem/settings.json to reset.'
});
return;
}
}
// Update all settings from request body
@@ -319,6 +328,31 @@ export class SettingsRoutes extends BaseRouteHandler {
}
}
// Validate CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES
if (settings.CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES) {
const count = parseInt(settings.CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES, 10);
if (isNaN(count) || count < 1 || count > 100) {
return { valid: false, error: 'CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES must be between 1 and 100' };
}
}
// Validate CLAUDE_MEM_OPENROUTER_MAX_TOKENS
if (settings.CLAUDE_MEM_OPENROUTER_MAX_TOKENS) {
const tokens = parseInt(settings.CLAUDE_MEM_OPENROUTER_MAX_TOKENS, 10);
if (isNaN(tokens) || tokens < 1000 || tokens > 1000000) {
return { valid: false, error: 'CLAUDE_MEM_OPENROUTER_MAX_TOKENS must be between 1000 and 1000000' };
}
}
// Validate CLAUDE_MEM_OPENROUTER_SITE_URL if provided
if (settings.CLAUDE_MEM_OPENROUTER_SITE_URL) {
try {
new URL(settings.CLAUDE_MEM_OPENROUTER_SITE_URL);
} catch {
return { valid: false, error: 'CLAUDE_MEM_OPENROUTER_SITE_URL must be a valid URL' };
}
}
// Skip observation types validation - any type string is valid since modes define their own types
// The database accepts any TEXT value, and mode-specific validation happens at parse time