refactor: Enhance session management and error handling in AgentService and related components

- Improved session handling by implementing ensureSession to load sessions from disk if not in memory, reducing "session not found" errors.
- Enhanced error messages for non-existent sessions, providing clearer diagnostics.
- Updated CodexProvider and OpencodeProvider to improve error handling and messaging.
- Refactored various routes to use async/await for better readability and error handling.
- Added event emission for merge and stash operations in the MergeService and StashService.
- Cleaned up error messages in AgentExecutor to remove redundant prefixes and ANSI codes for better clarity.
This commit is contained in:
gsxdsm
2026-02-18 17:30:12 -08:00
parent 6903d3c508
commit df9a6314da
22 changed files with 827 additions and 148 deletions

View File

@@ -123,9 +123,10 @@ describe('agent-service.ts', () => {
});
expect(result.success).toBe(true);
// First call reads session file, metadata file, and queue state file (3 calls)
// First call reads metadata file and session file via ensureSession (2 calls)
// Since no metadata or messages exist, a fresh session is created without loading queue state.
// Second call should reuse in-memory session (no additional calls)
expect(fs.readFile).toHaveBeenCalledTimes(3);
expect(fs.readFile).toHaveBeenCalledTimes(2);
});
});
@@ -330,14 +331,14 @@ describe('agent-service.ts', () => {
sessionId: 'session-1',
});
const history = service.getHistory('session-1');
const history = await service.getHistory('session-1');
expect(history).toBeDefined();
expect(history?.messages).toEqual([]);
});
it('should handle non-existent session', () => {
const history = service.getHistory('nonexistent');
it('should handle non-existent session', async () => {
const history = await service.getHistory('nonexistent');
expect(history).toBeDefined(); // Returns error object
});
});
@@ -356,10 +357,108 @@ describe('agent-service.ts', () => {
await service.clearSession('session-1');
const history = service.getHistory('session-1');
const history = await service.getHistory('session-1');
expect(history?.messages).toEqual([]);
expect(fs.writeFile).toHaveBeenCalled();
});
it('should clear sdkSessionId from persisted metadata to prevent stale session errors', async () => {
// Setup: Session exists in metadata with an sdkSessionId (simulating
// a session that previously communicated with a CLI provider like OpenCode)
const metadata = {
'session-1': {
id: 'session-1',
name: 'Test Session',
workingDirectory: '/test/dir',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
sdkSessionId: 'stale-opencode-session-id',
},
};
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(metadata));
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
// Start the session (loads from disk metadata)
await service.startConversation({
sessionId: 'session-1',
workingDirectory: '/test/dir',
});
// Clear the session
await service.clearSession('session-1');
// Verify that the LAST writeFile call to sessions-metadata.json
// (from clearSdkSessionId) has sdkSessionId removed.
// Earlier writes may still include it (e.g., from updateSessionTimestamp).
const writeFileCalls = vi.mocked(fs.writeFile).mock.calls;
const metadataWriteCalls = writeFileCalls.filter(
(call) =>
typeof call[0] === 'string' && (call[0] as string).includes('sessions-metadata.json')
);
expect(metadataWriteCalls.length).toBeGreaterThan(0);
const lastMetadataWriteCall = metadataWriteCalls[metadataWriteCalls.length - 1];
const savedMetadata = JSON.parse(lastMetadataWriteCall[1] as string);
expect(savedMetadata['session-1'].sdkSessionId).toBeUndefined();
});
});
describe('clearSdkSessionId', () => {
it('should remove sdkSessionId from persisted metadata', async () => {
const metadata = {
'session-1': {
id: 'session-1',
name: 'Test Session',
workingDirectory: '/test/dir',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
sdkSessionId: 'old-provider-session-id',
},
};
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(metadata));
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await service.clearSdkSessionId('session-1');
const writeFileCalls = vi.mocked(fs.writeFile).mock.calls;
expect(writeFileCalls.length).toBeGreaterThan(0);
const savedMetadata = JSON.parse(writeFileCalls[0][1] as string);
expect(savedMetadata['session-1'].sdkSessionId).toBeUndefined();
expect(savedMetadata['session-1'].updatedAt).not.toBe('2024-01-01T00:00:00Z');
});
it('should do nothing if session has no sdkSessionId', async () => {
const metadata = {
'session-1': {
id: 'session-1',
name: 'Test Session',
workingDirectory: '/test/dir',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
},
};
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(metadata));
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await service.clearSdkSessionId('session-1');
// writeFile should not have been called since there's no sdkSessionId to clear
expect(fs.writeFile).not.toHaveBeenCalled();
});
it('should do nothing if session does not exist in metadata', async () => {
vi.mocked(fs.readFile).mockResolvedValue('{}');
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await service.clearSdkSessionId('nonexistent');
expect(fs.writeFile).not.toHaveBeenCalled();
});
});
describe('createSession', () => {
@@ -654,15 +753,15 @@ describe('agent-service.ts', () => {
it('should return queue for session', async () => {
await service.addToQueue('session-1', { message: 'Test prompt' });
const result = service.getQueue('session-1');
const result = await service.getQueue('session-1');
expect(result.success).toBe(true);
expect(result.queue).toBeDefined();
expect(result.queue?.length).toBe(1);
});
it('should return error for non-existent session', () => {
const result = service.getQueue('nonexistent');
it('should return error for non-existent session', async () => {
const result = await service.getQueue('nonexistent');
expect(result.success).toBe(false);
expect(result.error).toBe('Session not found');
@@ -686,7 +785,7 @@ describe('agent-service.ts', () => {
});
it('should remove prompt from queue', async () => {
const queueResult = service.getQueue('session-1');
const queueResult = await service.getQueue('session-1');
const promptId = queueResult.queue![0].id;
const result = await service.removeFromQueue('session-1', promptId);
@@ -731,7 +830,7 @@ describe('agent-service.ts', () => {
const result = await service.clearQueue('session-1');
expect(result.success).toBe(true);
const queueResult = service.getQueue('session-1');
const queueResult = await service.getQueue('session-1');
expect(queueResult.queue?.length).toBe(0);
expect(mockEvents.emit).toHaveBeenCalled();
});