diff --git a/src/gateway/channel-health-monitor.test.ts b/src/gateway/channel-health-monitor.test.ts index c42d8d36aa0f..399bae437b02 100644 --- a/src/gateway/channel-health-monitor.test.ts +++ b/src/gateway/channel-health-monitor.test.ts @@ -456,6 +456,34 @@ describe("channel-health-monitor", () => { monitor.stop(); }); + it("continues pending recovery on the next check without waiting for cooldown", async () => { + const account: Partial = disconnectedAccount(Date.now() - 300_000); + const manager = createSnapshotManager( + { + discord: { + default: account, + }, + }, + { + startChannel: vi.fn(async () => { + account.running = false; + account.connected = false; + account.restartPending = true; + account.reconnectAttempts = 0; + }), + }, + ); + const monitor = await startAndRunCheck(manager); + expect(manager.stopChannel).toHaveBeenCalledTimes(1); + expect(manager.startChannel).toHaveBeenCalledTimes(1); + + await advanceHealthCheck(); + + expect(manager.stopChannel).toHaveBeenCalledTimes(1); + expect(manager.startChannel).toHaveBeenCalledTimes(2); + monitor.stop(); + }); + it("caps at 3 health-monitor restarts per channel per hour", async () => { const manager = createSnapshotManager({ discord: { diff --git a/src/gateway/channel-health-monitor.ts b/src/gateway/channel-health-monitor.ts index 7072325aa869..e3c822ced1df 100644 --- a/src/gateway/channel-health-monitor.ts +++ b/src/gateway/channel-health-monitor.ts @@ -145,12 +145,20 @@ export function startChannelHealthMonitor(deps: ChannelHealthMonitorDeps): Chann restartsThisHour: [], }; - if (now - record.lastRestartAt <= cooldownMs) { + const continuingPendingRestart = + status.running !== true && + status.restartPending === true && + (status.reconnectAttempts ?? 0) === 0; + + // A timed-out recovery stop uses the first start request to mark + // restartPending; the next monitor pass must finish that same recovery + // instead of waiting behind this monitor's fresh-restart cooldown. + if (!continuingPendingRestart && now - record.lastRestartAt <= cooldownMs) { continue; } pruneOldRestarts(record, now); - if (record.restartsThisHour.length >= maxRestartsPerHour) { + if (!continuingPendingRestart && record.restartsThisHour.length >= maxRestartsPerHour) { log.warn?.( `[${channelId}:${accountId}] health-monitor: hit ${maxRestartsPerHour} restarts/hour limit, skipping`, ); @@ -161,9 +169,11 @@ export function startChannelHealthMonitor(deps: ChannelHealthMonitorDeps): Chann log.info?.(`[${channelId}:${accountId}] health-monitor: restarting (reason: ${reason})`); - record.lastRestartAt = now; - record.restartsThisHour.push({ at: now }); - restartRecords.set(key, record); + if (!continuingPendingRestart) { + record.lastRestartAt = now; + record.restartsThisHour.push({ at: now }); + restartRecords.set(key, record); + } try { if (status.running) {