1- /**
2- * WebSocket Service for real-time plan execution streaming
3- */
1+ import { headerBuilder } from '../api/config' ;
42
53export interface StreamMessage {
6- type : 'plan_update' | 'step_update' | 'agent_message' | 'error' | 'connection_status' ;
4+ type : 'plan_update' | 'step_update' | 'agent_message' | 'error' | 'connection_status' | 'plan_approval_request' | 'final_result' ;
75 plan_id ?: string ;
86 session_id ?: string ;
97 data ?: any ;
@@ -12,40 +10,92 @@ export interface StreamMessage {
1210
1311export interface StreamingPlanUpdate {
1412 plan_id : string ;
15- session_id : string ;
13+ session_id ? : string ;
1614 step_id ?: string ;
1715 agent_name ?: string ;
1816 content ?: string ;
19- status ?: 'in_progress' | 'completed' | 'error' ;
20- message_type ?: 'thinking' | 'action' | 'result' | 'clarification_needed' ;
17+ status ?: 'in_progress' | 'completed' | 'error' | 'creating_plan' | 'pending_approval' ;
18+ message_type ?: 'thinking' | 'action' | 'result' | 'clarification_needed' | 'plan_approval_request' ;
19+ timestamp ?: number ;
20+ }
21+
22+ export interface PlanApprovalRequestData {
23+ plan_id : string ;
24+ session_id : string ;
25+ plan : {
26+ steps : Array < {
27+ id : string ;
28+ description : string ;
29+ agent : string ;
30+ estimated_duration ?: string ;
31+ } > ;
32+ total_steps : number ;
33+ estimated_completion ?: string ;
34+ } ;
35+ status : 'PENDING_APPROVAL' ;
36+ }
37+
38+ export interface PlanApprovalResponseData {
39+ plan_id : string ;
40+ session_id : string ;
41+ approved : boolean ;
42+ feedback ?: string ;
2143}
2244
2345class WebSocketService {
2446 private ws : WebSocket | null = null ;
2547 private reconnectAttempts = 0 ;
2648 private maxReconnectAttempts = 5 ;
27- private reconnectDelay = 1000 ;
49+ private reconnectDelay = 12000 ;
2850 private listeners : Map < string , Set < ( message : StreamMessage ) => void > > = new Map ( ) ;
2951 private planSubscriptions : Set < string > = new Set ( ) ;
52+ private reconnectTimer : NodeJS . Timeout | null = null ;
53+ private isConnecting = false ;
3054
3155 /**
3256 * Connect to WebSocket server
3357 */
3458 connect ( ) : Promise < void > {
3559 return new Promise ( ( resolve , reject ) => {
60+ if ( this . isConnecting ) {
61+ console . log ( 'Connection attempt already in progress' ) ;
62+ resolve ( ) ;
63+ return ;
64+ }
65+
66+ if ( this . reconnectTimer ) {
67+ clearTimeout ( this . reconnectTimer ) ;
68+ this . reconnectTimer = null ;
69+ }
70+
3671 try {
37- // Get WebSocket URL from environment or default to localhost
72+ this . isConnecting = true ;
73+
3874 const wsProtocol = window . location . protocol === 'https:' ? 'wss:' : 'ws:' ;
3975 const wsHost = process . env . REACT_APP_WS_HOST || '127.0.0.1:8000' ;
40- const wsUrl = `${ wsProtocol } //${ wsHost } /ws/streaming` ;
76+ const processId = crypto . randomUUID ( ) ;
77+
78+ const authHeaders = headerBuilder ( ) ;
79+ const userId = authHeaders [ 'x-ms-client-principal-id' ] ;
80+
81+ if ( ! userId ) {
82+ console . error ( 'No user ID available for WebSocket connection' ) ;
83+ this . isConnecting = false ;
84+ reject ( new Error ( 'Authentication required for WebSocket connection' ) ) ;
85+ return ;
86+ }
87+
88+ // Use query parameter for WebSocket authentication (as backend expects)
89+ const wsUrl = `${ wsProtocol } //${ wsHost } /api/v3/socket/${ processId } ?user_id=${ encodeURIComponent ( userId ) } ` ;
4190
4291 console . log ( 'Connecting to WebSocket:' , wsUrl ) ;
4392
4493 this . ws = new WebSocket ( wsUrl ) ;
45-
94+
4695 this . ws . onopen = ( ) => {
47- console . log ( 'WebSocket connected' ) ;
96+ console . log ( 'WebSocket connected successfully ' ) ;
4897 this . reconnectAttempts = 0 ;
98+ this . isConnecting = false ;
4999 this . emit ( 'connection_status' , { connected : true } ) ;
50100 resolve ( ) ;
51101 } ;
@@ -55,23 +105,30 @@ class WebSocketService {
55105 const message : StreamMessage = JSON . parse ( event . data ) ;
56106 this . handleMessage ( message ) ;
57107 } catch ( error ) {
58- console . error ( 'Error parsing WebSocket message:' , error ) ;
108+ console . error ( 'Error parsing WebSocket message:' , error , 'Raw data:' , event . data ) ;
109+ this . emit ( 'error' , { error : 'Failed to parse WebSocket message' } ) ;
59110 }
60111 } ;
61112
62- this . ws . onclose = ( ) => {
63- console . log ( 'WebSocket disconnected' ) ;
113+ this . ws . onclose = ( event ) => {
114+ console . log ( 'WebSocket disconnected' , event . code , event . reason ) ;
115+ this . isConnecting = false ;
64116 this . emit ( 'connection_status' , { connected : false } ) ;
65- this . attemptReconnect ( ) ;
117+
118+ if ( event . code !== 1000 ) {
119+ this . attemptReconnect ( ) ;
120+ }
66121 } ;
67122
68123 this . ws . onerror = ( error ) => {
69124 console . error ( 'WebSocket error:' , error ) ;
125+ this . isConnecting = false ;
70126 this . emit ( 'error' , { error : 'WebSocket connection failed' } ) ;
71127 reject ( error ) ;
72128 } ;
73129
74130 } catch ( error ) {
131+ this . isConnecting = false ;
75132 reject ( error ) ;
76133 }
77134 } ) ;
@@ -81,11 +138,21 @@ class WebSocketService {
81138 * Disconnect from WebSocket server
82139 */
83140 disconnect ( ) : void {
141+ console . log ( 'Manually disconnecting WebSocket' ) ;
142+
143+ if ( this . reconnectTimer ) {
144+ clearTimeout ( this . reconnectTimer ) ;
145+ this . reconnectTimer = null ;
146+ }
147+
148+ this . reconnectAttempts = this . maxReconnectAttempts ;
149+
84150 if ( this . ws ) {
85- this . ws . close ( ) ;
151+ this . ws . close ( 1000 , 'Manual disconnect' ) ;
86152 this . ws = null ;
87153 }
88154 this . planSubscriptions . clear ( ) ;
155+ this . isConnecting = false ;
89156 }
90157
91158 /**
@@ -130,7 +197,6 @@ class WebSocketService {
130197
131198 this . listeners . get ( eventType ) ! . add ( callback ) ;
132199
133- // Return unsubscribe function
134200 return ( ) => {
135201 const eventListeners = this . listeners . get ( eventType ) ;
136202 if ( eventListeners ) {
@@ -183,12 +249,14 @@ class WebSocketService {
183249 private handleMessage ( message : StreamMessage ) : void {
184250 console . log ( 'WebSocket message received:' , message ) ;
185251
186- // Emit to specific event listeners
187252 if ( message . type ) {
188253 this . emit ( message . type , message . data ) ;
189254 }
190255
191- // Emit to general message listeners
256+ if ( message . type === 'plan_approval_request' ) {
257+ console . log ( 'Plan approval request received via WebSocket:' , message . data ) ;
258+ }
259+
192260 this . emit ( 'message' , message ) ;
193261 }
194262
@@ -197,20 +265,28 @@ class WebSocketService {
197265 */
198266 private attemptReconnect ( ) : void {
199267 if ( this . reconnectAttempts >= this . maxReconnectAttempts ) {
200- console . log ( 'Max reconnection attempts reached' ) ;
268+ console . log ( 'Max reconnection attempts reached - stopping reconnect attempts ' ) ;
201269 this . emit ( 'error' , { error : 'Max reconnection attempts reached' } ) ;
202270 return ;
203271 }
204272
273+ if ( this . isConnecting || this . reconnectTimer ) {
274+ console . log ( 'Reconnection attempt already in progress' ) ;
275+ return ;
276+ }
277+
205278 this . reconnectAttempts ++ ;
206279 const delay = this . reconnectDelay * Math . pow ( 2 , this . reconnectAttempts - 1 ) ;
207280
208- console . log ( `Attempting to reconnect in ${ delay } ms (attempt ${ this . reconnectAttempts } ) ` ) ;
281+ console . log ( `Scheduling reconnection attempt ${ this . reconnectAttempts } / ${ this . maxReconnectAttempts } in ${ delay / 1000 } s ` ) ;
209282
210- setTimeout ( ( ) => {
283+ this . reconnectTimer = setTimeout ( ( ) => {
284+ this . reconnectTimer = null ;
285+ console . log ( `Attempting reconnection (attempt ${ this . reconnectAttempts } )` ) ;
286+
211287 this . connect ( )
212288 . then ( ( ) => {
213- // Re-subscribe to all plans
289+ console . log ( 'Reconnection successful - re-subscribing to plans' ) ;
214290 this . planSubscriptions . forEach ( planId => {
215291 this . subscribeToPlan ( planId ) ;
216292 } ) ;
@@ -238,8 +314,30 @@ class WebSocketService {
238314 console . warn ( 'WebSocket is not connected. Cannot send message:' , message ) ;
239315 }
240316 }
317+
318+ /**
319+ * Send plan approval response
320+ */
321+ sendPlanApprovalResponse ( response : PlanApprovalResponseData ) : void {
322+ if ( ! this . ws || this . ws . readyState !== WebSocket . OPEN ) {
323+ console . error ( 'WebSocket not connected - cannot send plan approval response' ) ;
324+ this . emit ( 'error' , { error : 'Cannot send plan approval response - WebSocket not connected' } ) ;
325+ return ;
326+ }
327+
328+ try {
329+ const message = {
330+ type : 'plan_approval_response' ,
331+ data : response
332+ } ;
333+ this . ws . send ( JSON . stringify ( message ) ) ;
334+ console . log ( 'Plan approval response sent:' , response ) ;
335+ } catch ( error ) {
336+ console . error ( 'Failed to send plan approval response:' , error ) ;
337+ this . emit ( 'error' , { error : 'Failed to send plan approval response' } ) ;
338+ }
339+ }
241340}
242341
243- // Export singleton instance
244342export const webSocketService = new WebSocketService ( ) ;
245343export default webSocketService ;
0 commit comments