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:
Alex Newman
2026-02-06 03:23:13 -05:00
parent 56df2c45e5
commit 5dffb1ebb0
5 changed files with 127 additions and 7 deletions
@@ -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