@@ -179,6 +179,7 @@ impl ExecutionBackend for RemoteExecutor {
179179 let cell_idx = cell_index. context ( "cell_index required for remote execution" ) ?;
180180 let ydoc = self . ydoc . as_mut ( ) . context ( "Y.js client not connected" ) ?;
181181 let http = reqwest:: Client :: new ( ) ;
182+ let client_writes = !ydoc. server_writes_outputs ( ) ;
182183
183184 // 1. Fire execute request
184185 let msg_id = ws
@@ -187,6 +188,7 @@ impl ExecutionBackend for RemoteExecutor {
187188
188189 // 2. Watch for changes on the ydoc for this cell
189190 let mut outputs: Vec < nbformat:: v4:: Output > = Vec :: new ( ) ;
191+ let mut kernel_outputs: Vec < nbformat:: v4:: Output > = Vec :: new ( ) ;
190192 let mut fetched_urls: HashSet < String > = HashSet :: new ( ) ;
191193 let mut seen_indices: HashSet < usize > = HashSet :: new ( ) ;
192194 let mut idle_received = false ;
@@ -227,28 +229,20 @@ impl ExecutionBackend for RemoteExecutor {
227229 }
228230
229231 if idle_received {
230- let has_error = outputs
231- . iter ( )
232- . any ( |o| matches ! ( o, nbformat:: v4:: Output :: Error ( _) ) ) ;
233- let error_info = outputs. iter ( ) . find_map ( |o| {
234- if let nbformat:: v4:: Output :: Error ( err) = o {
235- Some ( ExecutionError {
236- ename : err. ename . clone ( ) ,
237- evalue : err. evalue . clone ( ) ,
238- traceback : err. traceback . clone ( ) ,
239- } )
240- } else {
241- None
242- }
243- } ) ;
244- return if has_error {
245- Ok ( ExecutionResult :: error ( outputs, ec, error_info. unwrap ( ) ) )
246- } else {
247- Ok ( ExecutionResult :: success ( outputs, ec) )
248- } ;
232+ return Self :: build_result ( outputs, ec) ;
249233 }
250234 }
251235
236+ // When the server doesn't write outputs and kernel is done,
237+ // write collected outputs to Y.js ourselves, sync, then let
238+ // the read loop above pick them up on the next iteration.
239+ if client_writes && idle_received && !ec_ready && !kernel_outputs. is_empty ( ) {
240+ ydoc. update_cell_outputs ( cell_idx, kernel_outputs. clone ( ) ) ?;
241+ ydoc. update_cell_execution_count ( cell_idx, expected_ec) ?;
242+ ydoc. sync ( ) . await ?;
243+ continue ;
244+ }
245+
252246 // 4. Wait for new messages
253247 if idle_received {
254248 match tokio:: time:: timeout_at ( deadline, ydoc. recv_update ( ) ) . await {
@@ -273,7 +267,13 @@ impl ExecutionBackend for RemoteExecutor {
273267 idle_received = true ;
274268 }
275269 }
276- _ => { }
270+ _ => {
271+ if client_writes {
272+ if let Some ( output) = Self :: kernel_msg_to_output( & msg. content) {
273+ kernel_outputs. push( output) ;
274+ }
275+ }
276+ }
277277 }
278278 }
279279 }
@@ -285,6 +285,11 @@ impl ExecutionBackend for RemoteExecutor {
285285 }
286286 }
287287
288+ // Fallback: if we collected kernel outputs but never wrote them
289+ if client_writes && !kernel_outputs. is_empty ( ) {
290+ return Self :: build_result ( kernel_outputs, expected_ec) ;
291+ }
292+
288293 let ec = ydoc
289294 . read_cell_outputs ( cell_idx)
290295 . ok ( )
@@ -310,3 +315,70 @@ impl ExecutionBackend for RemoteExecutor {
310315 Ok ( ( ) )
311316 }
312317}
318+
319+ impl RemoteExecutor {
320+ fn build_result (
321+ outputs : Vec < nbformat:: v4:: Output > ,
322+ ec : Option < i64 > ,
323+ ) -> Result < ExecutionResult > {
324+ let has_error = outputs
325+ . iter ( )
326+ . any ( |o| matches ! ( o, nbformat:: v4:: Output :: Error ( _) ) ) ;
327+ let error_info = outputs. iter ( ) . find_map ( |o| {
328+ if let nbformat:: v4:: Output :: Error ( err) = o {
329+ Some ( ExecutionError {
330+ ename : err. ename . clone ( ) ,
331+ evalue : err. evalue . clone ( ) ,
332+ traceback : err. traceback . clone ( ) ,
333+ } )
334+ } else {
335+ None
336+ }
337+ } ) ;
338+ if has_error {
339+ Ok ( ExecutionResult :: error ( outputs, ec, error_info. unwrap ( ) ) )
340+ } else {
341+ Ok ( ExecutionResult :: success ( outputs, ec) )
342+ }
343+ }
344+
345+ fn kernel_msg_to_output ( content : & JupyterMessageContent ) -> Option < nbformat:: v4:: Output > {
346+ match content {
347+ JupyterMessageContent :: StreamContent ( stream) => {
348+ let name = match stream. name {
349+ jupyter_protocol:: Stdio :: Stdout => "stdout" . to_string ( ) ,
350+ jupyter_protocol:: Stdio :: Stderr => "stderr" . to_string ( ) ,
351+ } ;
352+ Some ( nbformat:: v4:: Output :: Stream {
353+ name,
354+ text : nbformat:: v4:: MultilineString ( stream. text . clone ( ) ) ,
355+ } )
356+ }
357+ JupyterMessageContent :: ExecuteResult ( result) => {
358+ let json = serde_json:: json!( {
359+ "output_type" : "execute_result" ,
360+ "execution_count" : result. execution_count. value( ) ,
361+ "data" : result. data,
362+ "metadata" : result. metadata
363+ } ) ;
364+ serde_json:: from_value ( json) . ok ( )
365+ }
366+ JupyterMessageContent :: DisplayData ( display) => {
367+ let json = serde_json:: json!( {
368+ "output_type" : "display_data" ,
369+ "data" : display. data,
370+ "metadata" : display. metadata
371+ } ) ;
372+ serde_json:: from_value ( json) . ok ( )
373+ }
374+ JupyterMessageContent :: ErrorOutput ( error) => {
375+ Some ( nbformat:: v4:: Output :: Error ( nbformat:: v4:: ErrorOutput {
376+ ename : error. ename . clone ( ) ,
377+ evalue : error. evalue . clone ( ) ,
378+ traceback : error. traceback . clone ( ) ,
379+ } ) )
380+ }
381+ _ => None ,
382+ }
383+ }
384+ }
0 commit comments