Suppress socket disconnect error on normal Claude Code exit

- Add isExitingNormally flag set during onExit callback
- Check for common normal disconnect errors (EPIPE, ECONNRESET, Unknown error)
- Log as debug instead of error for normal disconnects
- Prevents misleading error message when exiting Claude Code normally
This commit is contained in:
Peter Steinberger 2025-07-29 15:30:35 +02:00
parent 7a69ccb711
commit 5d49693573
3 changed files with 40 additions and 17 deletions

View file

@ -1,8 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DEFAULT_NOTIFICATION_PREFERENCES } from '../../types/config.js';
import { notificationEventService } from './notification-event-service.js';
import type { NotificationPreferences } from './push-notification-service.js';
import { pushNotificationService } from './push-notification-service.js';
import { notificationEventService } from './notification-event-service.js';
import { serverConfigService } from './server-config-service.js';
// Mock notification event service
@ -514,7 +514,9 @@ describe('PushNotificationService', () => {
vibrationEnabled: true,
};
(serverConfigService.updateNotificationPreferences as vi.Mock).mockRejectedValue(new Error('Network error'));
(serverConfigService.updateNotificationPreferences as vi.Mock).mockRejectedValue(
new Error('Network error')
);
await expect(pushNotificationService.savePreferences(preferences)).rejects.toThrow(
'Network error'
@ -543,8 +545,8 @@ describe('PushNotificationService', () => {
const testPromise = pushNotificationService.testNotification();
// Simulate receiving the event
await new Promise(resolve => setTimeout(resolve, 100)); // allow time for listener to be registered
await new Promise((resolve) => setTimeout(resolve, 100)); // allow time for listener to be registered
expect(testNotificationHandler!).toBeDefined();
testNotificationHandler!({
title: 'VibeTunnel Test',
@ -584,8 +586,8 @@ describe('PushNotificationService', () => {
const testPromise = pushNotificationService.testNotification();
// Simulate receiving the event
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(testNotificationHandler!).toBeDefined();
testNotificationHandler!({});

View file

@ -394,16 +394,13 @@ export class PushNotificationService {
// Show notification if we have permission
if (this.serviceWorkerRegistration && this.getPermission() === 'granted') {
await this.serviceWorkerRegistration.showNotification(
data.title || 'VibeTunnel Test',
{
body: data.body || 'Test notification received via SSE!',
icon: '/apple-touch-icon.png',
badge: '/favicon-32.png',
tag: 'vibetunnel-test-sse',
requireInteraction: false,
}
);
await this.serviceWorkerRegistration.showNotification(data.title || 'VibeTunnel Test', {
body: data.body || 'Test notification received via SSE!',
icon: '/apple-touch-icon.png',
badge: '/favicon-32.png',
tag: 'vibetunnel-test-sse',
requireInteraction: false,
});
logger.log('✅ Displayed SSE test notification');
}
resolve();

View file

@ -390,6 +390,7 @@ export async function startVibeTunnelForward(args: string[]) {
// Variables that need to be accessible in cleanup
let sessionFileWatcher: fs.FSWatcher | undefined;
let fileWatchDebounceTimer: NodeJS.Timeout | undefined;
let isExitingNormally = false;
const sessionOptions: Parameters<typeof ptyManager.createSession>[1] = {
sessionId: finalSessionId,
@ -405,6 +406,9 @@ export async function startVibeTunnelForward(args: string[]) {
gitIsWorktree: gitInfo.gitIsWorktree,
gitMainRepoPath: gitInfo.gitMainRepoPath,
onExit: async (exitCode: number) => {
// Mark that we're exiting normally
isExitingNormally = true;
// Show exit message
logger.log(
chalk.yellow(`\n✓ VibeTunnel session ended`) + chalk.gray(` (exit code: ${exitCode})`)
@ -737,7 +741,27 @@ export async function startVibeTunnelForward(args: string[]) {
// Handle socket events
socketClient.on('disconnect', (error) => {
logger.error('Socket disconnected:', error?.message || 'Unknown error');
// Don't log error if we're exiting normally
if (isExitingNormally) {
logger.debug('Socket disconnected during normal exit');
return;
}
// Check if this is a common disconnect error during normal operation
const errorMessage = error?.message || '';
const isNormalDisconnect =
errorMessage.includes('EPIPE') ||
errorMessage.includes('ECONNRESET') ||
errorMessage.includes('socket hang up') ||
errorMessage === 'Unknown error' || // Common during clean exits
!error; // No error object means clean disconnect
if (isNormalDisconnect) {
logger.debug('Socket disconnected (normal termination)');
} else {
logger.error('Socket disconnected:', error?.message || 'Unknown error');
}
process.exit(1);
});