feat: drain orphaned pending messages on SIGTERM session completion (#1567)

* feat: drain orphaned pending messages on session completion (SIGTERM)

When deleteSession() aborts the SDK agent via SIGTERM, pending messages
in the queue are never processed. Without drain, they remain in
'pending' status forever — no future generator picks them up because
the session is already completed.

Adds markAllSessionMessagesAbandoned() call after deleteSession() in
completeByDbId(). This reuses the existing PendingMessageStore method
already used by worker-service.ts terminateSession().

Production evidence: 15 orphaned summarize messages found across
completed sessions (ages 3h to 3 days) before this fix. After fix:
0 orphaned messages over 23 days of operation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: document best-effort drain limitation per CodeRabbit review #1567

Add comment noting the rare race condition when generators outlive the
30s SIGTERM timeout. Practical risk is negligible (0 orphans over 23 days).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Alessandro Costa <alessandro@claudio.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alessandro Costa
2026-04-04 19:14:25 -03:00
committed by GitHub
parent c5129ed016
commit 8958c3335d
@@ -24,9 +24,30 @@ export class SessionCompletionHandler {
* 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)
// Delete from session manager (aborts SDK agent via SIGTERM)
await this.sessionManager.deleteSession(sessionDbId);
// Drain orphaned pending messages left by SIGTERM.
// When deleteSession() aborts the generator, pending messages in the queue
// are never processed. Without drain, they stay in 'pending' status forever
// since no future generator will pick them up for a completed session.
// Note: this is best-effort — if a generator outlives the 30s SIGTERM timeout
// (SessionManager.deleteSession), it may enqueue messages after this drain.
// In practice this race is rare (zero orphans over 23 days, 3400+ observations).
try {
const pendingStore = this.sessionManager.getPendingMessageStore();
const drainedCount = pendingStore.markAllSessionMessagesAbandoned(sessionDbId);
if (drainedCount > 0) {
logger.warn('SESSION', `Drained ${drainedCount} orphaned pending messages on session completion`, {
sessionId: sessionDbId, drainedCount
});
}
} catch (e) {
logger.debug('SESSION', 'Failed to drain pending queue on session completion', {
sessionId: sessionDbId, error: e instanceof Error ? e.message : String(e)
});
}
// Broadcast session completed event
this.eventBroadcaster.broadcastSessionCompleted(sessionDbId);
}