Add connection resilience and reconnect commands for Kosmi and Jackbox

Kosmi WebSocket would silently die after hours/days with no reconnection.
Jackbox WebSocket failed to reconnect after API server restarts (stale JWT)
and leaked heartbeat goroutines on each reconnect cycle.

Kosmi changes:
- Add WebSocket ping/pong keepalive (30s ping, 90s read deadline)
- Send EventFailure on unexpected disconnect to trigger gateway reconnectBridge()
- Add intentionalDisconnect flag to prevent false failure events on clean shutdown
- Fix Disconnect() to be safe for reconnect cycles

Jackbox changes:
- Add read deadline (90s) to detect stale connections
- Fix heartbeat goroutine leak via per-connection listenDone channel
- Re-authenticate for fresh JWT before each reconnect attempt
- Add Manager.Reconnect() for on-demand teardown and rebuild

IRC commands:
- !kreconnect - reconnect Kosmi bridge
- !jreconnect - reconnect Jackbox WebSocket
- !reconnect  - reconnect all services (Kosmi + Jackbox)

Made-with: Cursor
This commit is contained in:
cottongin
2026-04-05 05:30:39 -04:00
parent bec3615d2b
commit 4fc7f08b24
6 changed files with 182 additions and 32 deletions

View File

@@ -242,6 +242,37 @@ func (m *Manager) monitorActiveSessions() {
}
}
// Reconnect tears down the existing WebSocket client and establishes a new
// connection (re-authenticating for a fresh JWT). Safe to call even when
// already disconnected.
func (m *Manager) Reconnect() error {
if !m.enabled || !m.useWebSocket {
return fmt.Errorf("Jackbox WebSocket is not enabled")
}
m.log.Info("Forcing Jackbox WebSocket reconnection...")
// Tear down existing client
if m.wsClient != nil {
if err := m.wsClient.Close(); err != nil {
m.log.Errorf("Error closing existing WebSocket client: %v", err)
}
m.wsClient = nil
}
// Re-authenticate for a fresh JWT
if err := m.client.Authenticate(); err != nil {
return fmt.Errorf("re-authentication failed: %w", err)
}
// Rebuild the WebSocket client using the original callback
if m.messageCallback == nil {
return fmt.Errorf("no message callback registered")
}
return m.startWebSocketClient(m.messageCallback)
}
// GetClient returns the Jackbox API client (may be nil if disabled)
func (m *Manager) GetClient() *Client {
return m.client