feat: multi-turn conversations and Claude fallback for Gemini provider
Major improvements to Gemini provider: **Shared Conversation History** - Add ConversationMessage interface for provider-agnostic history - Both Claude and Gemini agents read/write shared conversationHistory - Context persists across provider switches via claudeSessionId linkage **Multi-Turn Gemini API** - Replace stateless single-query with full conversation context - queryGeminiMultiTurn() sends entire history for coherent responses - Maps 'assistant' role to 'model' for Gemini API compatibility **Automatic Fallback to Claude** - Detect rate limits (429), server errors (5xx), network failures - Fall back to Claude SDK when Gemini API fails - Reset 'processing' messages to 'pending' before fallback **Mid-Session Provider Switching** - Track currentProvider on ActiveSession - Provider changes take effect after current generator finishes - Avoids race conditions from aborting active generators Files changed: - worker-types.ts: Add ConversationMessage, currentProvider tracking - GeminiAgent.ts: Multi-turn queries, fallback logic - SDKAgent.ts: Capture messages to shared history - SessionManager.ts: Initialize new session fields - SessionRoutes.ts: Provider selection and switching logic - worker-service.ts: Wire up fallback agent dependency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -85,6 +85,7 @@ export class WorkerService {
|
||||
this.sseBroadcaster = new SSEBroadcaster();
|
||||
this.sdkAgent = new SDKAgent(this.dbManager, this.sessionManager);
|
||||
this.geminiAgent = new GeminiAgent(this.dbManager, this.sessionManager);
|
||||
this.geminiAgent.setFallbackAgent(this.sdkAgent); // Enable fallback to Claude on Gemini API failure
|
||||
this.paginationHelper = new PaginationHelper(this.dbManager);
|
||||
this.settingsManager = new SettingsManager(this.dbManager);
|
||||
this.sessionEventBroadcaster = new SessionEventBroadcaster(this.sseBroadcaster, this);
|
||||
|
||||
@@ -8,6 +8,15 @@ import type { Response } from 'express';
|
||||
// Active Session Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Provider-agnostic conversation message for shared history
|
||||
* Used to maintain context across Claude↔Gemini provider switches
|
||||
*/
|
||||
export interface ConversationMessage {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ActiveSession {
|
||||
sessionDbId: number;
|
||||
claudeSessionId: string;
|
||||
@@ -22,6 +31,8 @@ export interface ActiveSession {
|
||||
cumulativeInputTokens: number; // Track input tokens for discovery cost
|
||||
cumulativeOutputTokens: number; // Track output tokens for discovery cost
|
||||
pendingProcessingIds: Set<number>; // Track ALL message IDs yielded but not yet processed
|
||||
conversationHistory: ConversationMessage[]; // Shared conversation history for provider switching
|
||||
currentProvider: 'claude' | 'gemini' | null; // Track which provider is currently running
|
||||
}
|
||||
|
||||
export interface PendingMessage {
|
||||
|
||||
@@ -19,7 +19,7 @@ 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, PendingMessage } from '../worker-types.js';
|
||||
import type { ActiveSession, PendingMessage, ConversationMessage } from '../worker-types.js';
|
||||
import { ModeManager } from '../domain/ModeManager.js';
|
||||
|
||||
// Gemini API endpoint
|
||||
@@ -43,17 +43,58 @@ interface GeminiResponse {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini content message format
|
||||
* role: "user" or "model" (Gemini uses "model" not "assistant")
|
||||
*/
|
||||
interface GeminiContent {
|
||||
role: 'user' | 'model';
|
||||
parts: Array<{ text: string }>;
|
||||
}
|
||||
|
||||
// Forward declaration for fallback agent type
|
||||
type FallbackAgent = {
|
||||
startSession(session: ActiveSession, worker?: any): Promise<void>;
|
||||
};
|
||||
|
||||
export class GeminiAgent {
|
||||
private dbManager: DatabaseManager;
|
||||
private sessionManager: SessionManager;
|
||||
private fallbackAgent: FallbackAgent | null = null;
|
||||
|
||||
constructor(dbManager: DatabaseManager, sessionManager: SessionManager) {
|
||||
this.dbManager = dbManager;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fallback agent (Claude SDK) for when Gemini API fails
|
||||
* Must be set after construction to avoid circular dependency
|
||||
*/
|
||||
setFallbackAgent(agent: FallbackAgent): void {
|
||||
this.fallbackAgent = agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error should trigger fallback to Claude
|
||||
*/
|
||||
private shouldFallbackToClaude(error: any): boolean {
|
||||
const message = error?.message || '';
|
||||
// Fall back on rate limit (429), server errors (5xx), or network issues
|
||||
return (
|
||||
message.includes('429') ||
|
||||
message.includes('500') ||
|
||||
message.includes('502') ||
|
||||
message.includes('503') ||
|
||||
message.includes('ECONNREFUSED') ||
|
||||
message.includes('ETIMEDOUT') ||
|
||||
message.includes('fetch failed')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Gemini agent for a session
|
||||
* Uses multi-turn conversation to maintain context across messages
|
||||
*/
|
||||
async startSession(session: ActiveSession, worker?: any): Promise<void> {
|
||||
try {
|
||||
@@ -72,10 +113,14 @@ export class GeminiAgent {
|
||||
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode);
|
||||
|
||||
// Query Gemini with initial prompt
|
||||
const initResponse = await this.queryGemini(initPrompt, apiKey, model);
|
||||
// Add to conversation history and query Gemini with full context
|
||||
session.conversationHistory.push({ role: 'user', content: initPrompt });
|
||||
const initResponse = await this.queryGeminiMultiTurn(session.conversationHistory, apiKey, model);
|
||||
|
||||
if (initResponse.content) {
|
||||
// Add response to conversation history
|
||||
session.conversationHistory.push({ role: 'assistant', content: initResponse.content });
|
||||
|
||||
// Track token usage
|
||||
const tokensUsed = initResponse.tokensUsed || 0;
|
||||
session.cumulativeInputTokens += Math.floor(tokensUsed * 0.7); // Rough estimate
|
||||
@@ -103,10 +148,14 @@ export class GeminiAgent {
|
||||
cwd: message.cwd
|
||||
});
|
||||
|
||||
// Query Gemini
|
||||
const obsResponse = await this.queryGemini(obsPrompt, apiKey, model);
|
||||
// Add to conversation history and query Gemini with full context
|
||||
session.conversationHistory.push({ role: 'user', content: obsPrompt });
|
||||
const obsResponse = await this.queryGeminiMultiTurn(session.conversationHistory, apiKey, model);
|
||||
|
||||
if (obsResponse.content) {
|
||||
// Add response to conversation history
|
||||
session.conversationHistory.push({ role: 'assistant', content: obsResponse.content });
|
||||
|
||||
const tokensUsed = obsResponse.tokensUsed || 0;
|
||||
session.cumulativeInputTokens += Math.floor(tokensUsed * 0.7);
|
||||
session.cumulativeOutputTokens += Math.floor(tokensUsed * 0.3);
|
||||
@@ -124,10 +173,14 @@ export class GeminiAgent {
|
||||
last_assistant_message: message.last_assistant_message || ''
|
||||
}, mode);
|
||||
|
||||
// Query Gemini
|
||||
const summaryResponse = await this.queryGemini(summaryPrompt, apiKey, model);
|
||||
// Add to conversation history and query Gemini with full context
|
||||
session.conversationHistory.push({ role: 'user', content: summaryPrompt });
|
||||
const summaryResponse = await this.queryGeminiMultiTurn(session.conversationHistory, apiKey, model);
|
||||
|
||||
if (summaryResponse.content) {
|
||||
// Add response to conversation history
|
||||
session.conversationHistory.push({ role: 'assistant', content: summaryResponse.content });
|
||||
|
||||
const tokensUsed = summaryResponse.tokensUsed || 0;
|
||||
session.cumulativeInputTokens += Math.floor(tokensUsed * 0.7);
|
||||
session.cumulativeOutputTokens += Math.floor(tokensUsed * 0.3);
|
||||
@@ -140,7 +193,8 @@ export class GeminiAgent {
|
||||
const sessionDuration = Date.now() - session.startTime;
|
||||
logger.success('SDK', 'Gemini agent completed', {
|
||||
sessionId: session.sessionDbId,
|
||||
duration: `${(sessionDuration / 1000).toFixed(1)}s`
|
||||
duration: `${(sessionDuration / 1000).toFixed(1)}s`,
|
||||
historyLength: session.conversationHistory.length
|
||||
});
|
||||
|
||||
this.dbManager.getSessionStore().markSessionCompleted(session.sessionDbId);
|
||||
@@ -148,25 +202,65 @@ export class GeminiAgent {
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
logger.warn('SDK', 'Gemini agent aborted', { sessionId: session.sessionDbId });
|
||||
} else {
|
||||
logger.failure('SDK', 'Gemini agent error', { sessionDbId: session.sessionDbId }, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check if we should fall back to Claude
|
||||
if (this.shouldFallbackToClaude(error) && this.fallbackAgent) {
|
||||
logger.warn('SDK', 'Gemini API failed, falling back to Claude SDK', {
|
||||
sessionDbId: session.sessionDbId,
|
||||
error: error.message,
|
||||
historyLength: session.conversationHistory.length
|
||||
});
|
||||
|
||||
// Reset any 'processing' messages back to 'pending' so Claude can retry them
|
||||
// This handles the case where Gemini failed mid-processing a message
|
||||
const pendingStore = this.sessionManager.getPendingMessageStore();
|
||||
const resetCount = pendingStore.resetStuckMessages(0); // 0 = reset ALL processing messages
|
||||
if (resetCount > 0) {
|
||||
logger.info('SDK', 'Reset processing messages for fallback', {
|
||||
sessionDbId: session.sessionDbId,
|
||||
resetCount
|
||||
});
|
||||
}
|
||||
|
||||
// Fall back to Claude - it will use the same session with shared conversationHistory
|
||||
// Note: Claude SDK will continue processing from current state
|
||||
return this.fallbackAgent.startSession(session, worker);
|
||||
}
|
||||
|
||||
logger.failure('SDK', 'Gemini agent error', { sessionDbId: session.sessionDbId }, error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Cleanup
|
||||
this.sessionManager.deleteSession(session.sessionDbId).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Gemini via REST API
|
||||
* Convert shared ConversationMessage array to Gemini's contents format
|
||||
* Maps 'assistant' role to 'model' for Gemini API compatibility
|
||||
*/
|
||||
private async queryGemini(
|
||||
prompt: string,
|
||||
private conversationToGeminiContents(history: ConversationMessage[]): GeminiContent[] {
|
||||
return history.map(msg => ({
|
||||
role: msg.role === 'assistant' ? 'model' : 'user',
|
||||
parts: [{ text: msg.content }]
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Gemini via REST API with full conversation history (multi-turn)
|
||||
* Sends the entire conversation context for coherent responses
|
||||
*/
|
||||
private async queryGeminiMultiTurn(
|
||||
history: ConversationMessage[],
|
||||
apiKey: string,
|
||||
model: GeminiModel
|
||||
): Promise<{ content: string; tokensUsed?: number }> {
|
||||
logger.debug('SDK', `Querying Gemini (${model})`, { promptLength: prompt.length });
|
||||
const contents = this.conversationToGeminiContents(history);
|
||||
const totalChars = history.reduce((sum, m) => sum + m.content.length, 0);
|
||||
|
||||
logger.debug('SDK', `Querying Gemini multi-turn (${model})`, {
|
||||
turns: history.length,
|
||||
totalChars
|
||||
});
|
||||
|
||||
const url = `${GEMINI_API_URL}/${model}:generateContent?key=${apiKey}`;
|
||||
|
||||
@@ -176,9 +270,7 @@ export class GeminiAgent {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [{
|
||||
parts: [{ text: prompt }]
|
||||
}],
|
||||
contents,
|
||||
generationConfig: {
|
||||
temperature: 0.3, // Lower temperature for structured extraction
|
||||
maxOutputTokens: 4096,
|
||||
|
||||
@@ -184,20 +184,31 @@ export class SDKAgent {
|
||||
* - SessionManager.initializeSession already fetched this from database
|
||||
* - Database row was created by new-hook's createSDKSession call
|
||||
* - We just use the session_id we're given - simple and reliable
|
||||
*
|
||||
* SHARED CONVERSATION HISTORY:
|
||||
* - Each user message is added to session.conversationHistory
|
||||
* - This allows provider switching (Claude→Gemini) with full context
|
||||
* - SDK manages its own internal state, but we mirror it for interop
|
||||
*/
|
||||
private async *createMessageGenerator(session: ActiveSession): AsyncIterableIterator<SDKUserMessage> {
|
||||
// Load active mode
|
||||
const mode = ModeManager.getInstance().getActiveMode();
|
||||
|
||||
// Build initial prompt
|
||||
const initPrompt = session.lastPromptNumber === 1
|
||||
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode);
|
||||
|
||||
// Add to shared conversation history for provider interop
|
||||
session.conversationHistory.push({ role: 'user', content: initPrompt });
|
||||
|
||||
// Yield initial user prompt with context (or continuation if prompt #2+)
|
||||
// CRITICAL: Both paths use session.claudeSessionId from the hook
|
||||
yield {
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: session.lastPromptNumber === 1
|
||||
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode)
|
||||
content: initPrompt
|
||||
},
|
||||
session_id: session.claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
@@ -212,36 +223,46 @@ export class SDKAgent {
|
||||
session.lastPromptNumber = message.prompt_number;
|
||||
}
|
||||
|
||||
const obsPrompt = buildObservationPrompt({
|
||||
id: 0, // Not used in prompt
|
||||
tool_name: message.tool_name!,
|
||||
tool_input: JSON.stringify(message.tool_input),
|
||||
tool_output: JSON.stringify(message.tool_response),
|
||||
created_at_epoch: Date.now(),
|
||||
cwd: message.cwd
|
||||
});
|
||||
|
||||
// Add to shared conversation history for provider interop
|
||||
session.conversationHistory.push({ role: 'user', content: obsPrompt });
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: buildObservationPrompt({
|
||||
id: 0, // Not used in prompt
|
||||
tool_name: message.tool_name!,
|
||||
tool_input: JSON.stringify(message.tool_input),
|
||||
tool_output: JSON.stringify(message.tool_response),
|
||||
created_at_epoch: Date.now(),
|
||||
cwd: message.cwd
|
||||
})
|
||||
content: obsPrompt
|
||||
},
|
||||
session_id: session.claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
isSynthetic: true
|
||||
};
|
||||
} else if (message.type === 'summarize') {
|
||||
const summaryPrompt = buildSummaryPrompt({
|
||||
id: session.sessionDbId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
last_assistant_message: message.last_assistant_message || ''
|
||||
}, mode);
|
||||
|
||||
// Add to shared conversation history for provider interop
|
||||
session.conversationHistory.push({ role: 'user', content: summaryPrompt });
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: buildSummaryPrompt({
|
||||
id: session.sessionDbId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
last_assistant_message: message.last_assistant_message || ''
|
||||
}, mode)
|
||||
content: summaryPrompt
|
||||
},
|
||||
session_id: session.claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
@@ -254,8 +275,16 @@ export class SDKAgent {
|
||||
/**
|
||||
* Process SDK response text (parse XML, save to database, sync to Chroma)
|
||||
* @param discoveryTokens - Token cost for discovering this response (delta, not cumulative)
|
||||
*
|
||||
* Also captures assistant responses to shared conversation history for provider interop.
|
||||
* This allows Gemini to see full context if provider is switched mid-session.
|
||||
*/
|
||||
private async processSDKResponse(session: ActiveSession, text: string, worker: any | undefined, discoveryTokens: number): Promise<void> {
|
||||
// Add assistant response to shared conversation history for provider interop
|
||||
if (text) {
|
||||
session.conversationHistory.push({ role: 'assistant', content: text });
|
||||
}
|
||||
|
||||
// Parse observations
|
||||
const observations = parseObservations(text, session.claudeSessionId);
|
||||
|
||||
|
||||
@@ -117,7 +117,9 @@ export class SessionManager {
|
||||
startTime: Date.now(),
|
||||
cumulativeInputTokens: 0,
|
||||
cumulativeOutputTokens: 0,
|
||||
pendingProcessingIds: new Set()
|
||||
pendingProcessingIds: new Set(),
|
||||
conversationHistory: [], // Initialize empty - will be populated by agents
|
||||
currentProvider: null // Will be set when generator starts
|
||||
};
|
||||
|
||||
this.sessions.set(sessionDbId, session);
|
||||
|
||||
@@ -42,7 +42,10 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
|
||||
/**
|
||||
* Get the appropriate agent based on settings
|
||||
* Falls back to Claude SDK if Gemini is selected but not configured
|
||||
* Throws error if Gemini is selected but not configured (no silent fallback)
|
||||
*
|
||||
* Note: Session linking via claudeSessionId allows provider switching mid-session.
|
||||
* The conversationHistory on ActiveSession maintains context across providers.
|
||||
*/
|
||||
private getActiveAgent(): SDKAgent | GeminiAgent {
|
||||
if (isGeminiSelected()) {
|
||||
@@ -50,36 +53,82 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
logger.debug('SESSION', 'Using Gemini agent');
|
||||
return this.geminiAgent;
|
||||
} else {
|
||||
logger.warn('SESSION', 'Gemini selected but no API key configured, falling back to Claude SDK');
|
||||
return this.sdkAgent;
|
||||
throw new Error('Gemini provider selected but no API key configured. Set CLAUDE_MEM_GEMINI_API_KEY in settings or GEMINI_API_KEY environment variable.');
|
||||
}
|
||||
}
|
||||
return this.sdkAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected provider name
|
||||
*/
|
||||
private getSelectedProvider(): 'claude' | 'gemini' {
|
||||
return (isGeminiSelected() && isGeminiAvailable()) ? 'gemini' : 'claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures agent generator is running for a session
|
||||
* Auto-starts if not already running to process pending queue
|
||||
* Uses either Claude SDK or Gemini based on settings
|
||||
*
|
||||
* Provider switching: If provider setting changed while generator is running,
|
||||
* we let the current generator finish naturally (max 5s linger timeout).
|
||||
* The next generator will use the new provider with shared conversationHistory.
|
||||
*/
|
||||
private ensureGeneratorRunning(sessionDbId: number, source: string): void {
|
||||
const session = this.sessionManager.getSession(sessionDbId);
|
||||
if (session && !session.generatorPromise) {
|
||||
const agent = this.getActiveAgent();
|
||||
const agentName = (isGeminiSelected() && isGeminiAvailable()) ? 'Gemini' : 'Claude SDK';
|
||||
if (!session) return;
|
||||
|
||||
logger.info('SESSION', `Generator auto-starting (${source}) using ${agentName}`, {
|
||||
sessionId: sessionDbId,
|
||||
queueDepth: session.pendingMessages.length
|
||||
});
|
||||
const selectedProvider = this.getSelectedProvider();
|
||||
|
||||
session.generatorPromise = agent.startSession(session, this.workerService)
|
||||
.finally(() => {
|
||||
logger.info('SESSION', `Generator finished`, { sessionId: sessionDbId });
|
||||
session.generatorPromise = null;
|
||||
this.workerService.broadcastProcessingStatus();
|
||||
});
|
||||
// Start generator if not running
|
||||
if (!session.generatorPromise) {
|
||||
this.startGeneratorWithProvider(session, selectedProvider, source);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generator is running - check if provider changed
|
||||
if (session.currentProvider && session.currentProvider !== selectedProvider) {
|
||||
logger.info('SESSION', `Provider changed, will switch after current generator finishes`, {
|
||||
sessionId: sessionDbId,
|
||||
currentProvider: session.currentProvider,
|
||||
selectedProvider,
|
||||
historyLength: session.conversationHistory.length
|
||||
});
|
||||
// Let current generator finish naturally, next one will use new provider
|
||||
// The shared conversationHistory ensures context is preserved
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a generator with the specified provider
|
||||
*/
|
||||
private startGeneratorWithProvider(
|
||||
session: ReturnType<typeof this.sessionManager.getSession>,
|
||||
provider: 'claude' | 'gemini',
|
||||
source: string
|
||||
): void {
|
||||
if (!session) return;
|
||||
|
||||
const agent = provider === 'gemini' ? this.geminiAgent : this.sdkAgent;
|
||||
const agentName = provider === 'gemini' ? 'Gemini' : 'Claude SDK';
|
||||
|
||||
logger.info('SESSION', `Generator auto-starting (${source}) using ${agentName}`, {
|
||||
sessionId: session.sessionDbId,
|
||||
queueDepth: session.pendingMessages.length,
|
||||
historyLength: session.conversationHistory.length
|
||||
});
|
||||
|
||||
// Track which provider is running
|
||||
session.currentProvider = provider;
|
||||
|
||||
session.generatorPromise = agent.startSession(session, this.workerService)
|
||||
.finally(() => {
|
||||
logger.info('SESSION', `Generator finished`, { sessionId: session.sessionDbId });
|
||||
session.generatorPromise = null;
|
||||
session.currentProvider = null;
|
||||
this.workerService.broadcastProcessingStatus();
|
||||
});
|
||||
}
|
||||
|
||||
setupRoutes(app: express.Application): void {
|
||||
@@ -150,24 +199,8 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
});
|
||||
}
|
||||
|
||||
// Start agent in background (pass worker ref for spinner control)
|
||||
const agent = this.getActiveAgent();
|
||||
const agentName = isGeminiSelected() ? 'Gemini' : 'Claude SDK';
|
||||
|
||||
logger.info('SESSION', `Generator starting using ${agentName}`, {
|
||||
sessionId: sessionDbId,
|
||||
project: session.project,
|
||||
promptNum: session.lastPromptNumber
|
||||
});
|
||||
|
||||
session.generatorPromise = agent.startSession(session, this.workerService)
|
||||
.finally(() => {
|
||||
// Clear generator reference when completed
|
||||
logger.info('SESSION', `Generator finished`, { sessionId: sessionDbId });
|
||||
session.generatorPromise = null;
|
||||
// Broadcast status change (generator finished, may stop spinner)
|
||||
this.workerService.broadcastProcessingStatus();
|
||||
});
|
||||
// Start agent in background using the helper method
|
||||
this.startGeneratorWithProvider(session, this.getSelectedProvider(), 'init');
|
||||
|
||||
// Broadcast session started event
|
||||
this.eventBroadcaster.broadcastSessionStarted(sessionDbId, session.project);
|
||||
|
||||
Reference in New Issue
Block a user