MAESTRO: fix(hooks): add session-complete handler to enable orphan reaper cleanup
Cherry-picked from PR #844 by @thusdigital. Sessions stayed in active sessions map forever after summarize, causing the orphan reaper to think all processes were still active. Adds session-complete as Stop phase 2 hook that calls POST /api/sessions/complete to remove sessions from the active map, allowing the reaper to correctly identify and clean up orphaned worker processes. Fixes #842. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -290,6 +290,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
app.post('/api/sessions/init', this.handleSessionInitByClaudeId.bind(this));
|
||||
app.post('/api/sessions/observations', this.handleObservationsByClaudeId.bind(this));
|
||||
app.post('/api/sessions/summarize', this.handleSummarizeByClaudeId.bind(this));
|
||||
app.post('/api/sessions/complete', this.handleCompleteByClaudeId.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -594,6 +595,54 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
res.json({ status: 'queued' });
|
||||
});
|
||||
|
||||
/**
|
||||
* Complete session by contentSessionId (session-complete hook uses this)
|
||||
* POST /api/sessions/complete
|
||||
* Body: { contentSessionId }
|
||||
*
|
||||
* Removes session from active sessions map, allowing orphan reaper to
|
||||
* clean up any remaining subprocesses.
|
||||
*
|
||||
* Fixes Issue #842: Sessions stay in map forever, reaper thinks all active.
|
||||
*/
|
||||
private handleCompleteByClaudeId = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||
const { contentSessionId } = req.body;
|
||||
|
||||
logger.info('HTTP', '→ POST /api/sessions/complete', { contentSessionId });
|
||||
|
||||
if (!contentSessionId) {
|
||||
return this.badRequest(res, 'Missing contentSessionId');
|
||||
}
|
||||
|
||||
const store = this.dbManager.getSessionStore();
|
||||
|
||||
// Look up sessionDbId from contentSessionId (createSDKSession is idempotent)
|
||||
// Pass empty strings - we only need the ID lookup, not to create a new session
|
||||
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
|
||||
|
||||
// Check if session is in the active sessions map
|
||||
const activeSession = this.sessionManager.getSession(sessionDbId);
|
||||
if (!activeSession) {
|
||||
// Session may not be in memory (already completed or never initialized)
|
||||
logger.debug('SESSION', 'session-complete: Session not in active map', {
|
||||
contentSessionId,
|
||||
sessionDbId
|
||||
});
|
||||
res.json({ status: 'skipped', reason: 'not_active' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete the session (removes from active sessions map)
|
||||
await this.completionHandler.completeByDbId(sessionDbId);
|
||||
|
||||
logger.info('SESSION', 'Session completed via API', {
|
||||
contentSessionId,
|
||||
sessionDbId
|
||||
});
|
||||
|
||||
res.json({ status: 'completed', sessionDbId });
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize session by contentSessionId (new-hook uses this)
|
||||
* POST /api/sessions/init
|
||||
|
||||
Reference in New Issue
Block a user