Avoid persisting empty resume sessions (#27770)

This commit is contained in:
Sandy Tao
2026-06-09 15:41:47 -07:00
committed by GitHub
parent 4523560278
commit 3a13b8eeb6
9 changed files with 318 additions and 199 deletions
+74 -142
View File
@@ -9,7 +9,6 @@ import {
SessionSelector,
extractFirstUserMessage,
formatRelativeTime,
hasUserOrAssistantMessage,
SessionError,
convertSessionToHistoryFormats,
} from './sessionUtils.js';
@@ -512,6 +511,80 @@ describe('SessionSelector', () => {
expect(sessions[0].id).toBe(sessionIdWithUser);
});
it('should not list command-only sessions', async () => {
const commandOnlySessionId = randomUUID();
const chatsDir = path.join(tmpDir, 'chats');
await fs.mkdir(chatsDir, { recursive: true });
const metadata = {
sessionId: commandOnlySessionId,
projectHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:01:00.000Z',
};
const commandMessage = {
type: 'user',
content: '/resume',
id: 'msg1',
timestamp: '2024-01-01T10:00:30.000Z',
};
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${commandOnlySessionId.slice(0, 8)}.jsonl`,
),
`${JSON.stringify(metadata)}\n${JSON.stringify(commandMessage)}\n`,
);
const sessionSelector = new SessionSelector(storage);
const sessions = await sessionSelector.listSessions();
expect(sessions).toEqual([]);
});
it('should use the first non-command user message for display', async () => {
const sessionId = randomUUID();
const chatsDir = path.join(tmpDir, 'chats');
await fs.mkdir(chatsDir, { recursive: true });
const metadata = {
sessionId,
projectHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:02:00.000Z',
};
const commandMessage = {
type: 'user',
content: '/resume',
id: 'msg1',
timestamp: '2024-01-01T10:00:30.000Z',
};
const realMessage = {
type: 'user',
content: 'Help me fix resume history',
id: 'msg2',
timestamp: '2024-01-01T10:01:00.000Z',
};
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId.slice(0, 8)}.jsonl`,
),
`${JSON.stringify(metadata)}\n${JSON.stringify(commandMessage)}\n${JSON.stringify(realMessage)}\n`,
);
const sessionSelector = new SessionSelector(storage);
const sessions = await sessionSelector.listSessions();
expect(sessions).toHaveLength(1);
expect(sessions[0].firstUserMessage).toBe('Help me fix resume history');
expect(sessions[0].displayName).toBe('Help me fix resume history');
});
it('should list session with gemini message even without user message', async () => {
const sessionIdGeminiOnly = randomUUID();
@@ -781,147 +854,6 @@ describe('extractFirstUserMessage', () => {
});
});
describe('hasUserOrAssistantMessage', () => {
it('should return true when session has user message', () => {
const messages = [
{
type: 'user',
content: 'Hello',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
] as MessageRecord[];
expect(hasUserOrAssistantMessage(messages)).toBe(true);
});
it('should return true when session has gemini message', () => {
const messages = [
{
type: 'gemini',
content: 'Hello, how can I help?',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
] as MessageRecord[];
expect(hasUserOrAssistantMessage(messages)).toBe(true);
});
it('should return true when session has both user and gemini messages', () => {
const messages = [
{
type: 'user',
content: 'Hello',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
{
type: 'gemini',
content: 'Hi there!',
id: 'msg2',
timestamp: '2024-01-01T10:01:00.000Z',
},
] as MessageRecord[];
expect(hasUserOrAssistantMessage(messages)).toBe(true);
});
it('should return false when session only has info messages', () => {
const messages = [
{
type: 'info',
content: 'Session started',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
] as MessageRecord[];
expect(hasUserOrAssistantMessage(messages)).toBe(false);
});
it('should return false when session only has error messages', () => {
const messages = [
{
type: 'error',
content: 'An error occurred',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
] as MessageRecord[];
expect(hasUserOrAssistantMessage(messages)).toBe(false);
});
it('should return false when session only has warning messages', () => {
const messages = [
{
type: 'warning',
content: 'Warning message',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
] as MessageRecord[];
expect(hasUserOrAssistantMessage(messages)).toBe(false);
});
it('should return false when session only has system messages (mixed)', () => {
const messages = [
{
type: 'info',
content: 'Session started',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
{
type: 'error',
content: 'An error occurred',
id: 'msg2',
timestamp: '2024-01-01T10:01:00.000Z',
},
{
type: 'warning',
content: 'Warning message',
id: 'msg3',
timestamp: '2024-01-01T10:02:00.000Z',
},
] as MessageRecord[];
expect(hasUserOrAssistantMessage(messages)).toBe(false);
});
it('should return true when session has user message among system messages', () => {
const messages = [
{
type: 'info',
content: 'Session started',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
{
type: 'user',
content: 'Hello',
id: 'msg2',
timestamp: '2024-01-01T10:01:00.000Z',
},
{
type: 'error',
content: 'An error occurred',
id: 'msg3',
timestamp: '2024-01-01T10:02:00.000Z',
},
] as MessageRecord[];
expect(hasUserOrAssistantMessage(messages)).toBe(true);
});
it('should return false for empty messages array', () => {
const messages: MessageRecord[] = [];
expect(hasUserOrAssistantMessage(messages)).toBe(false);
});
});
describe('formatRelativeTime', () => {
it('should format time correctly', () => {
const now = new Date();
+4 -11
View File
@@ -139,15 +139,6 @@ export interface SessionSelectionResult {
displayInfo: string;
}
/**
* Checks if a session has at least one user or assistant (gemini) message.
* Sessions with only system messages (info, error, warning) are considered empty.
* @param messages - The array of message records to check
* @returns true if the session has meaningful content
*/
export const hasUserOrAssistantMessage = (messages: MessageRecord[]): boolean =>
messages.some((msg) => msg.type === 'user' || msg.type === 'gemini');
/**
* Cleans and sanitizes message content for display by:
* - Converting newlines to spaces
@@ -287,8 +278,10 @@ export const getAllSessionFiles = async (
const lastUpdated =
content.lastUpdated || content.startTime || fallbackTimestamp;
// Skip sessions that only contain system messages (info, error, warning)
if (!content.hasUserOrAssistantMessage) {
// Skip sessions with no resumable conversation content, including
// startup-only, system-only, command-only, and internal-context-only
// sessions.
if (!content.hasResumableContent) {
return { fileName: file, sessionInfo: null };
}