mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(core): gracefully handle disk full errors in chat recording (#17305)
Co-authored-by: RUiNtheExtinct <deepkarma001@gmail.com> Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
@@ -454,4 +454,159 @@ describe('ChatRecordingService', () => {
|
|||||||
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ENOSPC (disk full) graceful degradation - issue #16266', () => {
|
||||||
|
it('should disable recording and not throw when ENOSPC occurs during initialize', () => {
|
||||||
|
const enospcError = new Error('ENOSPC: no space left on device');
|
||||||
|
(enospcError as NodeJS.ErrnoException).code = 'ENOSPC';
|
||||||
|
|
||||||
|
mkdirSyncSpy.mockImplementation(() => {
|
||||||
|
throw enospcError;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
expect(() => chatRecordingService.initialize()).not.toThrow();
|
||||||
|
|
||||||
|
// Recording should be disabled (conversationFile set to null)
|
||||||
|
expect(chatRecordingService.getConversationFilePath()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable recording and not throw when ENOSPC occurs during writeConversation', () => {
|
||||||
|
chatRecordingService.initialize();
|
||||||
|
|
||||||
|
const enospcError = new Error('ENOSPC: no space left on device');
|
||||||
|
(enospcError as NodeJS.ErrnoException).code = 'ENOSPC';
|
||||||
|
|
||||||
|
vi.spyOn(fs, 'readFileSync').mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
sessionId: 'test-session-id',
|
||||||
|
projectHash: 'test-project-hash',
|
||||||
|
messages: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSyncSpy.mockImplementation(() => {
|
||||||
|
throw enospcError;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not throw when recording a message
|
||||||
|
expect(() =>
|
||||||
|
chatRecordingService.recordMessage({
|
||||||
|
type: 'user',
|
||||||
|
content: 'Hello',
|
||||||
|
model: 'gemini-pro',
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
|
||||||
|
// Recording should be disabled (conversationFile set to null)
|
||||||
|
expect(chatRecordingService.getConversationFilePath()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip recording operations when recording is disabled', () => {
|
||||||
|
chatRecordingService.initialize();
|
||||||
|
|
||||||
|
const enospcError = new Error('ENOSPC: no space left on device');
|
||||||
|
(enospcError as NodeJS.ErrnoException).code = 'ENOSPC';
|
||||||
|
|
||||||
|
vi.spyOn(fs, 'readFileSync').mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
sessionId: 'test-session-id',
|
||||||
|
projectHash: 'test-project-hash',
|
||||||
|
messages: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// First call throws ENOSPC
|
||||||
|
writeFileSyncSpy.mockImplementationOnce(() => {
|
||||||
|
throw enospcError;
|
||||||
|
});
|
||||||
|
|
||||||
|
chatRecordingService.recordMessage({
|
||||||
|
type: 'user',
|
||||||
|
content: 'First message',
|
||||||
|
model: 'gemini-pro',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset mock to track subsequent calls
|
||||||
|
writeFileSyncSpy.mockClear();
|
||||||
|
|
||||||
|
// Subsequent calls should be no-ops (not call writeFileSync)
|
||||||
|
chatRecordingService.recordMessage({
|
||||||
|
type: 'user',
|
||||||
|
content: 'Second message',
|
||||||
|
model: 'gemini-pro',
|
||||||
|
});
|
||||||
|
|
||||||
|
chatRecordingService.recordThought({
|
||||||
|
subject: 'Test',
|
||||||
|
description: 'Test thought',
|
||||||
|
});
|
||||||
|
|
||||||
|
chatRecordingService.saveSummary('Test summary');
|
||||||
|
|
||||||
|
// writeFileSync should not have been called for any of these
|
||||||
|
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null from getConversation when recording is disabled', () => {
|
||||||
|
chatRecordingService.initialize();
|
||||||
|
|
||||||
|
const enospcError = new Error('ENOSPC: no space left on device');
|
||||||
|
(enospcError as NodeJS.ErrnoException).code = 'ENOSPC';
|
||||||
|
|
||||||
|
vi.spyOn(fs, 'readFileSync').mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
sessionId: 'test-session-id',
|
||||||
|
projectHash: 'test-project-hash',
|
||||||
|
messages: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSyncSpy.mockImplementation(() => {
|
||||||
|
throw enospcError;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger ENOSPC
|
||||||
|
chatRecordingService.recordMessage({
|
||||||
|
type: 'user',
|
||||||
|
content: 'Hello',
|
||||||
|
model: 'gemini-pro',
|
||||||
|
});
|
||||||
|
|
||||||
|
// getConversation should return null when disabled
|
||||||
|
expect(chatRecordingService.getConversation()).toBeNull();
|
||||||
|
expect(chatRecordingService.getConversationFilePath()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still throw for non-ENOSPC errors', () => {
|
||||||
|
chatRecordingService.initialize();
|
||||||
|
|
||||||
|
const otherError = new Error('Permission denied');
|
||||||
|
(otherError as NodeJS.ErrnoException).code = 'EACCES';
|
||||||
|
|
||||||
|
vi.spyOn(fs, 'readFileSync').mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
sessionId: 'test-session-id',
|
||||||
|
projectHash: 'test-project-hash',
|
||||||
|
messages: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSyncSpy.mockImplementation(() => {
|
||||||
|
throw otherError;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should throw for non-ENOSPC errors
|
||||||
|
expect(() =>
|
||||||
|
chatRecordingService.recordMessage({
|
||||||
|
type: 'user',
|
||||||
|
content: 'Hello',
|
||||||
|
model: 'gemini-pro',
|
||||||
|
}),
|
||||||
|
).toThrow('Permission denied');
|
||||||
|
|
||||||
|
// Recording should NOT be disabled for non-ENOSPC errors (file path still exists)
|
||||||
|
expect(chatRecordingService.getConversationFilePath()).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ import type { ToolResultDisplay } from '../tools/tools.js';
|
|||||||
|
|
||||||
export const SESSION_FILE_PREFIX = 'session-';
|
export const SESSION_FILE_PREFIX = 'session-';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warning message shown when recording is disabled due to disk full.
|
||||||
|
*/
|
||||||
|
const ENOSPC_WARNING_MESSAGE =
|
||||||
|
'Chat recording disabled: No space left on device. ' +
|
||||||
|
'The conversation will continue but will not be saved to disk. ' +
|
||||||
|
'Free up disk space and restart to enable recording.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token usage summary for a message or conversation.
|
* Token usage summary for a message or conversation.
|
||||||
*/
|
*/
|
||||||
@@ -173,6 +181,16 @@ export class ChatRecordingService {
|
|||||||
this.queuedThoughts = [];
|
this.queuedThoughts = [];
|
||||||
this.queuedTokens = null;
|
this.queuedTokens = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Handle disk full (ENOSPC) gracefully - disable recording but allow CLI to continue
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
'code' in error &&
|
||||||
|
(error as NodeJS.ErrnoException).code === 'ENOSPC'
|
||||||
|
) {
|
||||||
|
this.conversationFile = null;
|
||||||
|
debugLogger.warn(ENOSPC_WARNING_MESSAGE);
|
||||||
|
return; // Don't throw - allow the CLI to continue
|
||||||
|
}
|
||||||
debugLogger.error('Error initializing chat recording service:', error);
|
debugLogger.error('Error initializing chat recording service:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -425,6 +443,16 @@ export class ChatRecordingService {
|
|||||||
fs.writeFileSync(this.conversationFile, newContent);
|
fs.writeFileSync(this.conversationFile, newContent);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Handle disk full (ENOSPC) gracefully - disable recording but allow conversation to continue
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
'code' in error &&
|
||||||
|
(error as NodeJS.ErrnoException).code === 'ENOSPC'
|
||||||
|
) {
|
||||||
|
this.conversationFile = null;
|
||||||
|
debugLogger.warn(ENOSPC_WARNING_MESSAGE);
|
||||||
|
return; // Don't throw - allow the conversation to continue
|
||||||
|
}
|
||||||
debugLogger.error('Error writing conversation file.', error);
|
debugLogger.error('Error writing conversation file.', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -474,7 +502,7 @@ export class ChatRecordingService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the path to the current conversation file.
|
* Gets the path to the current conversation file.
|
||||||
* Returns null if the service hasn't been initialized yet.
|
* Returns null if the service hasn't been initialized yet or recording is disabled.
|
||||||
*/
|
*/
|
||||||
getConversationFilePath(): string | null {
|
getConversationFilePath(): string | null {
|
||||||
return this.conversationFile;
|
return this.conversationFile;
|
||||||
|
|||||||
Reference in New Issue
Block a user