feat: Introduce SessionEventBroadcaster and SessionCompletionHandler for improved session management
- Added SessionEventBroadcaster to handle broadcasting of session lifecycle events, consolidating SSE broadcasting and processing status updates. - Refactored SessionRoutes to utilize SessionEventBroadcaster for broadcasting events related to new prompts, session starts, and completions. - Created SessionCompletionHandler to centralize session completion logic, reducing duplication across multiple endpoints. - Updated WorkerService to initialize SessionEventBroadcaster and pass it to SessionRoutes.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -24,6 +24,7 @@ import { SettingsManager } from './worker/SettingsManager.js';
|
|||||||
import { SearchManager } from './worker/SearchManager.js';
|
import { SearchManager } from './worker/SearchManager.js';
|
||||||
import { FormattingService } from './worker/FormattingService.js';
|
import { FormattingService } from './worker/FormattingService.js';
|
||||||
import { TimelineService } from './worker/TimelineService.js';
|
import { TimelineService } from './worker/TimelineService.js';
|
||||||
|
import { SessionEventBroadcaster } from './worker/events/SessionEventBroadcaster.js';
|
||||||
|
|
||||||
// Import HTTP layer
|
// Import HTTP layer
|
||||||
import { createMiddleware, summarizeRequestBody as summarizeBody } from './worker/http/middleware.js';
|
import { createMiddleware, summarizeRequestBody as summarizeBody } from './worker/http/middleware.js';
|
||||||
@@ -46,6 +47,7 @@ export class WorkerService {
|
|||||||
private sdkAgent: SDKAgent;
|
private sdkAgent: SDKAgent;
|
||||||
private paginationHelper: PaginationHelper;
|
private paginationHelper: PaginationHelper;
|
||||||
private settingsManager: SettingsManager;
|
private settingsManager: SettingsManager;
|
||||||
|
private sessionEventBroadcaster: SessionEventBroadcaster;
|
||||||
|
|
||||||
// Route handlers
|
// Route handlers
|
||||||
private viewerRoutes: ViewerRoutes;
|
private viewerRoutes: ViewerRoutes;
|
||||||
@@ -64,6 +66,7 @@ export class WorkerService {
|
|||||||
this.sdkAgent = new SDKAgent(this.dbManager, this.sessionManager);
|
this.sdkAgent = new SDKAgent(this.dbManager, this.sessionManager);
|
||||||
this.paginationHelper = new PaginationHelper(this.dbManager);
|
this.paginationHelper = new PaginationHelper(this.dbManager);
|
||||||
this.settingsManager = new SettingsManager(this.dbManager);
|
this.settingsManager = new SettingsManager(this.dbManager);
|
||||||
|
this.sessionEventBroadcaster = new SessionEventBroadcaster(this.sseBroadcaster, this);
|
||||||
|
|
||||||
// Set callback for when sessions are deleted (to update activity indicator)
|
// Set callback for when sessions are deleted (to update activity indicator)
|
||||||
this.sessionManager.setOnSessionDeleted(() => {
|
this.sessionManager.setOnSessionDeleted(() => {
|
||||||
@@ -78,7 +81,7 @@ export class WorkerService {
|
|||||||
|
|
||||||
// Initialize route handlers (SearchRoutes will use MCP client initially, then switch to SearchManager after DB init)
|
// Initialize route handlers (SearchRoutes will use MCP client initially, then switch to SearchManager after DB init)
|
||||||
this.viewerRoutes = new ViewerRoutes(this.sseBroadcaster, this.dbManager, this.sessionManager);
|
this.viewerRoutes = new ViewerRoutes(this.sseBroadcaster, this.dbManager, this.sessionManager);
|
||||||
this.sessionRoutes = new SessionRoutes(this.sessionManager, this.dbManager, this.sdkAgent, this.sseBroadcaster, this);
|
this.sessionRoutes = new SessionRoutes(this.sessionManager, this.dbManager, this.sdkAgent, this.sessionEventBroadcaster, this);
|
||||||
this.dataRoutes = new DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime);
|
this.dataRoutes = new DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime);
|
||||||
// SearchRoutes needs SearchManager which requires initialized DB - will be created in initializeBackground()
|
// SearchRoutes needs SearchManager which requires initialized DB - will be created in initializeBackground()
|
||||||
this.searchRoutes = null;
|
this.searchRoutes = null;
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Session Event Broadcaster
|
||||||
|
*
|
||||||
|
* Provides semantic broadcast methods for session lifecycle events.
|
||||||
|
* Consolidates SSE broadcasting and processing status updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SSEBroadcaster } from '../SSEBroadcaster.js';
|
||||||
|
import type { WorkerService } from '../../worker-service.js';
|
||||||
|
|
||||||
|
export class SessionEventBroadcaster {
|
||||||
|
constructor(
|
||||||
|
private sseBroadcaster: SSEBroadcaster,
|
||||||
|
private workerService: WorkerService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast new user prompt arrival
|
||||||
|
* Starts activity indicator to show work is beginning
|
||||||
|
*/
|
||||||
|
broadcastNewPrompt(prompt: {
|
||||||
|
id: number;
|
||||||
|
claude_session_id: string;
|
||||||
|
project: string;
|
||||||
|
prompt_number: number;
|
||||||
|
prompt_text: string;
|
||||||
|
created_at_epoch: number;
|
||||||
|
}): void {
|
||||||
|
// Broadcast prompt details
|
||||||
|
this.sseBroadcaster.broadcast({
|
||||||
|
type: 'new_prompt',
|
||||||
|
prompt
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start activity indicator (work is about to begin)
|
||||||
|
this.sseBroadcaster.broadcast({
|
||||||
|
type: 'processing_status',
|
||||||
|
isProcessing: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update processing status based on queue depth
|
||||||
|
this.workerService.broadcastProcessingStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast session initialization
|
||||||
|
*/
|
||||||
|
broadcastSessionStarted(sessionDbId: number, project: string): void {
|
||||||
|
this.sseBroadcaster.broadcast({
|
||||||
|
type: 'session_started',
|
||||||
|
sessionDbId,
|
||||||
|
project
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update processing status
|
||||||
|
this.workerService.broadcastProcessingStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast observation queued
|
||||||
|
* Updates processing status to reflect new queue depth
|
||||||
|
*/
|
||||||
|
broadcastObservationQueued(sessionDbId: number): void {
|
||||||
|
this.sseBroadcaster.broadcast({
|
||||||
|
type: 'observation_queued',
|
||||||
|
sessionDbId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update processing status (queue depth changed)
|
||||||
|
this.workerService.broadcastProcessingStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast session completion
|
||||||
|
* Updates processing status to reflect session removal
|
||||||
|
*/
|
||||||
|
broadcastSessionCompleted(sessionDbId: number): void {
|
||||||
|
this.sseBroadcaster.broadcast({
|
||||||
|
type: 'session_completed',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
sessionDbId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update processing status (session removed from queue)
|
||||||
|
this.workerService.broadcastProcessingStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast summarize request queued
|
||||||
|
* Updates processing status to reflect new queue depth
|
||||||
|
*/
|
||||||
|
broadcastSummarizeQueued(): void {
|
||||||
|
// Update processing status (queue depth changed)
|
||||||
|
this.workerService.broadcastProcessingStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,19 +12,27 @@ import { stripMemoryTagsFromJson } from '../../../../utils/tag-stripping.js';
|
|||||||
import { SessionManager } from '../../SessionManager.js';
|
import { SessionManager } from '../../SessionManager.js';
|
||||||
import { DatabaseManager } from '../../DatabaseManager.js';
|
import { DatabaseManager } from '../../DatabaseManager.js';
|
||||||
import { SDKAgent } from '../../SDKAgent.js';
|
import { SDKAgent } from '../../SDKAgent.js';
|
||||||
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
|
||||||
import type { WorkerService } from '../../../worker-service.js';
|
import type { WorkerService } from '../../../worker-service.js';
|
||||||
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||||
|
import { SessionEventBroadcaster } from '../../events/SessionEventBroadcaster.js';
|
||||||
|
import { SessionCompletionHandler } from '../../session/SessionCompletionHandler.js';
|
||||||
|
|
||||||
export class SessionRoutes extends BaseRouteHandler {
|
export class SessionRoutes extends BaseRouteHandler {
|
||||||
|
private completionHandler: SessionCompletionHandler;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private sessionManager: SessionManager,
|
private sessionManager: SessionManager,
|
||||||
private dbManager: DatabaseManager,
|
private dbManager: DatabaseManager,
|
||||||
private sdkAgent: SDKAgent,
|
private sdkAgent: SDKAgent,
|
||||||
private sseBroadcaster: SSEBroadcaster,
|
private eventBroadcaster: SessionEventBroadcaster,
|
||||||
private workerService: WorkerService
|
private workerService: WorkerService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.completionHandler = new SessionCompletionHandler(
|
||||||
|
sessionManager,
|
||||||
|
dbManager,
|
||||||
|
eventBroadcaster
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,22 +89,13 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
|
|
||||||
// Broadcast new prompt to SSE clients (for web UI)
|
// Broadcast new prompt to SSE clients (for web UI)
|
||||||
if (latestPrompt) {
|
if (latestPrompt) {
|
||||||
this.sseBroadcaster.broadcast({
|
this.eventBroadcaster.broadcastNewPrompt({
|
||||||
type: 'new_prompt',
|
|
||||||
prompt: {
|
|
||||||
id: latestPrompt.id,
|
id: latestPrompt.id,
|
||||||
claude_session_id: latestPrompt.claude_session_id,
|
claude_session_id: latestPrompt.claude_session_id,
|
||||||
project: latestPrompt.project,
|
project: latestPrompt.project,
|
||||||
prompt_number: latestPrompt.prompt_number,
|
prompt_number: latestPrompt.prompt_number,
|
||||||
prompt_text: latestPrompt.prompt_text,
|
prompt_text: latestPrompt.prompt_text,
|
||||||
created_at_epoch: latestPrompt.created_at_epoch
|
created_at_epoch: latestPrompt.created_at_epoch
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start activity indicator immediately when prompt arrives (work is about to begin)
|
|
||||||
this.sseBroadcaster.broadcast({
|
|
||||||
type: 'processing_status',
|
|
||||||
isProcessing: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync user prompt to Chroma with error logging
|
// Sync user prompt to Chroma with error logging
|
||||||
@@ -127,9 +126,6 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast processing status (based on queue depth)
|
|
||||||
this.workerService.broadcastProcessingStatus();
|
|
||||||
|
|
||||||
// Start SDK agent in background (pass worker ref for spinner control)
|
// Start SDK agent in background (pass worker ref for spinner control)
|
||||||
logger.info('SESSION', 'Generator starting', {
|
logger.info('SESSION', 'Generator starting', {
|
||||||
sessionId: sessionDbId,
|
sessionId: sessionDbId,
|
||||||
@@ -149,12 +145,8 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
this.workerService.broadcastProcessingStatus();
|
this.workerService.broadcastProcessingStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Broadcast SSE event
|
// Broadcast session started event
|
||||||
this.sseBroadcaster.broadcast({
|
this.eventBroadcaster.broadcastSessionStarted(sessionDbId, session.project);
|
||||||
type: 'session_started',
|
|
||||||
sessionDbId,
|
|
||||||
project: session.project
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ status: 'initialized', sessionDbId, port: getWorkerPort() });
|
res.json({ status: 'initialized', sessionDbId, port: getWorkerPort() });
|
||||||
});
|
});
|
||||||
@@ -180,14 +172,8 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
// CRITICAL: Ensure SDK agent is running to consume the queue
|
// CRITICAL: Ensure SDK agent is running to consume the queue
|
||||||
this.ensureGeneratorRunning(sessionDbId, 'observation');
|
this.ensureGeneratorRunning(sessionDbId, 'observation');
|
||||||
|
|
||||||
// Broadcast activity status (queue depth changed)
|
// Broadcast observation queued event
|
||||||
this.workerService.broadcastProcessingStatus();
|
this.eventBroadcaster.broadcastObservationQueued(sessionDbId);
|
||||||
|
|
||||||
// Broadcast SSE event
|
|
||||||
this.sseBroadcaster.broadcast({
|
|
||||||
type: 'observation_queued',
|
|
||||||
sessionDbId
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ status: 'queued' });
|
res.json({ status: 'queued' });
|
||||||
});
|
});
|
||||||
@@ -207,8 +193,8 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
// CRITICAL: Ensure SDK agent is running to consume the queue
|
// CRITICAL: Ensure SDK agent is running to consume the queue
|
||||||
this.ensureGeneratorRunning(sessionDbId, 'summarize');
|
this.ensureGeneratorRunning(sessionDbId, 'summarize');
|
||||||
|
|
||||||
// Broadcast activity status (queue depth changed)
|
// Broadcast summarize queued event
|
||||||
this.workerService.broadcastProcessingStatus();
|
this.eventBroadcaster.broadcastSummarizeQueued();
|
||||||
|
|
||||||
res.json({ status: 'queued' });
|
res.json({ status: 'queued' });
|
||||||
});
|
});
|
||||||
@@ -243,16 +229,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||||
if (sessionDbId === null) return;
|
if (sessionDbId === null) return;
|
||||||
|
|
||||||
await this.sessionManager.deleteSession(sessionDbId);
|
await this.completionHandler.completeByDbId(sessionDbId);
|
||||||
|
|
||||||
// Mark session complete in database
|
|
||||||
this.dbManager.markSessionComplete(sessionDbId);
|
|
||||||
|
|
||||||
// Broadcast SSE event
|
|
||||||
this.sseBroadcaster.broadcast({
|
|
||||||
type: 'session_completed',
|
|
||||||
sessionDbId
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ status: 'deleted' });
|
res.json({ status: 'deleted' });
|
||||||
});
|
});
|
||||||
@@ -265,20 +242,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||||
if (sessionDbId === null) return;
|
if (sessionDbId === null) return;
|
||||||
|
|
||||||
await this.sessionManager.deleteSession(sessionDbId);
|
await this.completionHandler.completeByDbId(sessionDbId);
|
||||||
|
|
||||||
// Mark session complete in database
|
|
||||||
this.dbManager.markSessionComplete(sessionDbId);
|
|
||||||
|
|
||||||
// Broadcast processing status (based on queue depth)
|
|
||||||
this.workerService.broadcastProcessingStatus();
|
|
||||||
|
|
||||||
// Broadcast SSE event
|
|
||||||
this.sseBroadcaster.broadcast({
|
|
||||||
type: 'session_completed',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
sessionDbId
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
@@ -345,14 +309,8 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
// Ensure SDK agent is running
|
// Ensure SDK agent is running
|
||||||
this.ensureGeneratorRunning(sessionDbId, 'observation');
|
this.ensureGeneratorRunning(sessionDbId, 'observation');
|
||||||
|
|
||||||
// Broadcast activity status
|
// Broadcast observation queued event
|
||||||
this.workerService.broadcastProcessingStatus();
|
this.eventBroadcaster.broadcastObservationQueued(sessionDbId);
|
||||||
|
|
||||||
// Broadcast SSE event
|
|
||||||
this.sseBroadcaster.broadcast({
|
|
||||||
type: 'observation_queued',
|
|
||||||
sessionDbId
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ status: 'queued' });
|
res.json({ status: 'queued' });
|
||||||
});
|
});
|
||||||
@@ -394,8 +352,8 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
// Ensure SDK agent is running
|
// Ensure SDK agent is running
|
||||||
this.ensureGeneratorRunning(sessionDbId, 'summarize');
|
this.ensureGeneratorRunning(sessionDbId, 'summarize');
|
||||||
|
|
||||||
// Broadcast activity status
|
// Broadcast summarize queued event
|
||||||
this.workerService.broadcastProcessingStatus();
|
this.eventBroadcaster.broadcastSummarizeQueued();
|
||||||
|
|
||||||
res.json({ status: 'queued' });
|
res.json({ status: 'queued' });
|
||||||
});
|
});
|
||||||
@@ -414,34 +372,14 @@ export class SessionRoutes extends BaseRouteHandler {
|
|||||||
return this.badRequest(res, 'Missing claudeSessionId');
|
return this.badRequest(res, 'Missing claudeSessionId');
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = this.dbManager.getSessionStore();
|
const found = await this.completionHandler.completeByClaudeId(claudeSessionId);
|
||||||
|
|
||||||
// Find session by claudeSessionId
|
if (!found) {
|
||||||
const session = store.findActiveSDKSession(claudeSessionId);
|
|
||||||
if (!session) {
|
|
||||||
// No active session - nothing to clean up (may have already been completed)
|
// No active session - nothing to clean up (may have already been completed)
|
||||||
res.json({ success: true, message: 'No active session found' });
|
res.json({ success: true, message: 'No active session found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionDbId = session.id;
|
|
||||||
|
|
||||||
// Delete from session manager (aborts SDK agent)
|
|
||||||
await this.sessionManager.deleteSession(sessionDbId);
|
|
||||||
|
|
||||||
// Mark session complete in database
|
|
||||||
this.dbManager.markSessionComplete(sessionDbId);
|
|
||||||
|
|
||||||
// Broadcast processing status
|
|
||||||
this.workerService.broadcastProcessingStatus();
|
|
||||||
|
|
||||||
// Broadcast SSE event
|
|
||||||
this.sseBroadcaster.broadcast({
|
|
||||||
type: 'session_completed',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
sessionDbId
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Session Completion Handler
|
||||||
|
*
|
||||||
|
* Consolidates session completion logic to eliminate duplication across
|
||||||
|
* three different completion endpoints (DELETE, POST by DB ID, POST by Claude ID).
|
||||||
|
*
|
||||||
|
* All completion flows follow the same pattern:
|
||||||
|
* 1. Delete session from SessionManager (aborts SDK agent)
|
||||||
|
* 2. Mark session complete in database
|
||||||
|
* 3. Broadcast session completed event
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SessionManager } from '../SessionManager.js';
|
||||||
|
import { DatabaseManager } from '../DatabaseManager.js';
|
||||||
|
import { SessionEventBroadcaster } from '../events/SessionEventBroadcaster.js';
|
||||||
|
|
||||||
|
export class SessionCompletionHandler {
|
||||||
|
constructor(
|
||||||
|
private sessionManager: SessionManager,
|
||||||
|
private dbManager: DatabaseManager,
|
||||||
|
private eventBroadcaster: SessionEventBroadcaster
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete session by database ID
|
||||||
|
* Used by DELETE /api/sessions/:id and POST /api/sessions/:id/complete
|
||||||
|
*/
|
||||||
|
async completeByDbId(sessionDbId: number): Promise<void> {
|
||||||
|
// Delete from session manager (aborts SDK agent)
|
||||||
|
await this.sessionManager.deleteSession(sessionDbId);
|
||||||
|
|
||||||
|
// Mark session complete in database
|
||||||
|
this.dbManager.markSessionComplete(sessionDbId);
|
||||||
|
|
||||||
|
// Broadcast session completed event
|
||||||
|
this.eventBroadcaster.broadcastSessionCompleted(sessionDbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete session by Claude session ID
|
||||||
|
* Used by POST /api/sessions/complete (cleanup-hook endpoint)
|
||||||
|
*
|
||||||
|
* @returns true if session was found and completed, false if no active session found
|
||||||
|
*/
|
||||||
|
async completeByClaudeId(claudeSessionId: string): Promise<boolean> {
|
||||||
|
const store = this.dbManager.getSessionStore();
|
||||||
|
|
||||||
|
// Find session by claudeSessionId
|
||||||
|
const session = store.findActiveSDKSession(claudeSessionId);
|
||||||
|
if (!session) {
|
||||||
|
// No active session - nothing to clean up (may have already been completed)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionDbId = session.id;
|
||||||
|
|
||||||
|
// Complete using standard flow
|
||||||
|
await this.completeByDbId(sessionDbId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user