diff --git a/packages/cli/src/acp/acpClient.test.ts b/packages/cli/src/acp/acpClient.test.ts index f077b0ef4b..470ff38351 100644 --- a/packages/cli/src/acp/acpClient.test.ts +++ b/packages/cli/src/acp/acpClient.test.ts @@ -875,6 +875,32 @@ describe('Session', () => { expect(result).toMatchObject({ stopReason: 'end_turn' }); }); + it('should handle prompt with no finish reason (InvalidStreamError)', async () => { + mockChat.sendMessageStream.mockRejectedValue( + new InvalidStreamError('No finish reason', 'NO_FINISH_REASON'), + ); + + const result = await session.prompt({ + sessionId: 'session-1', + prompt: [{ type: 'text', text: 'Hi' }], + }); + + expect(mockChat.sendMessageStream).toHaveBeenCalled(); + expect(result).toMatchObject({ stopReason: 'end_turn' }); + }); + + it('should handle prompt with no finish reason (NO_FINISH_REASON anomaly)', async () => { + mockChat.sendMessageStream.mockRejectedValue({ type: 'NO_FINISH_REASON' }); + + const result = await session.prompt({ + sessionId: 'session-1', + prompt: [{ type: 'text', text: 'Hi' }], + }); + + expect(mockChat.sendMessageStream).toHaveBeenCalled(); + expect(result).toMatchObject({ stopReason: 'end_turn' }); + }); + it('should handle /memory command', async () => { const handleCommandSpy = vi .spyOn( diff --git a/packages/cli/src/acp/acpClient.ts b/packages/cli/src/acp/acpClient.ts index 14761d7162..e0a352e0d1 100644 --- a/packages/cli/src/acp/acpClient.ts +++ b/packages/cli/src/acp/acpClient.ts @@ -863,7 +863,10 @@ export class Session { (error && typeof error === 'object' && 'type' in error && - error.type === 'NO_RESPONSE_TEXT') + (error.type === 'NO_RESPONSE_TEXT' || + error.type === 'NO_FINISH_REASON' || + error.type === 'MALFORMED_FUNCTION_CALL' || + error.type === 'UNEXPECTED_TOOL_CALL')) ) { // The stream ended with an empty response or malformed tool call. // Treat this as a graceful end to the model's turn rather than a crash.