@@ -24,6 +24,38 @@ import { PassThrough, Writable } from "node:stream";
2424
2525const SYMBOL_PREFIX = "copilot_runtime_" ;
2626
27+ /**
28+ * Temporary, env-gated tracing (COPILOT_FFI_TRACE=1) for diagnosing in-process FFI
29+ * transport stalls. Logs frame writes/completions/inbound arrivals with a monotonic
30+ * sequence and the JSON-RPC id/method, plus a periodic heartbeat, so we can tell
31+ * whether a stalled round-trip is stuck on the write, on inbound delivery, or on the
32+ * event loop parking. Writes to stderr to avoid interleaving with JSON-RPC stdout.
33+ */
34+ const FFI_TRACE = process . env . COPILOT_FFI_TRACE === "1" ;
35+ function ffiTrace ( message : string ) : void {
36+ if ( FFI_TRACE ) {
37+ process . stderr . write ( `[ffi ${ Date . now ( ) % 100000 } pid=${ process . pid } ] ${ message } \n` ) ;
38+ }
39+ }
40+
41+ /** Extracts a compact `id/method` tag from a JSON-RPC frame for trace correlation. */
42+ function frameTag ( frame : Buffer ) : string {
43+ if ( ! FFI_TRACE ) {
44+ return "" ;
45+ }
46+ try {
47+ const text = frame . toString ( "utf8" ) ;
48+ const bodyStart = text . indexOf ( "\r\n\r\n" ) ;
49+ const body = bodyStart >= 0 ? text . slice ( bodyStart + 4 ) : text ;
50+ const parsed = JSON . parse ( body ) as { id ?: unknown ; method ?: unknown } ;
51+ const id = parsed . id !== undefined ? `id=${ JSON . stringify ( parsed . id ) } ` : "" ;
52+ const method = parsed . method !== undefined ? `m=${ String ( parsed . method ) } ` : "" ;
53+ return [ id , method ] . filter ( Boolean ) . join ( " " ) || "(no id/method)" ;
54+ } catch {
55+ return `(unparsed ${ frame . length } B)` ;
56+ }
57+ }
58+
2759type KoffiFunction = ReturnType < ReturnType < typeof koffi . load > [ "func" ] > ;
2860type KoffiType = ReturnType < typeof koffi . pointer > ;
2961type KoffiRegisteredCallback = ReturnType < typeof koffi . register > ;
@@ -273,14 +305,23 @@ export class FfiRuntimeHost {
273305 throw new Error ( "copilot_runtime_connection_open failed." ) ;
274306 }
275307
276- this . keepAlive = setInterval ( ( ) => { } , FfiRuntimeHost . INBOUND_PUMP_INTERVAL_MS ) ;
308+ let heartbeat = 0 ;
309+ this . keepAlive = setInterval ( ( ) => {
310+ if ( FFI_TRACE && ++ heartbeat % 250 === 0 ) {
311+ ffiTrace ( `~tick ${ heartbeat } pending=${ this . inboundQueue . length } ` ) ;
312+ }
313+ } , FfiRuntimeHost . INBOUND_PUMP_INTERVAL_MS ) ;
277314 }
278315
316+ private writeSeq = 0 ;
317+
279318 private writeFrame ( frame : Buffer , callback : ( error ?: Error | null ) => void ) : void {
280319 if ( this . disposed || ! this . connectionId ) {
281320 callback ( new Error ( "The in-process runtime connection is closed." ) ) ;
282321 return ;
283322 }
323+ const seq = ++ this . writeSeq ;
324+ ffiTrace ( `>>W seq=${ seq } len=${ frame . length } ${ frameTag ( frame ) } ` ) ;
284325 // Asynchronous FFI call: runs on a libuv worker thread and invokes this callback
285326 // when the native write completes, keeping the JS event loop free to service
286327 // inbound native→JS callbacks in the meantime (see the sendStream comment).
@@ -289,6 +330,7 @@ export class FfiRuntimeHost {
289330 frame ,
290331 frame . length ,
291332 ( error : Error | null , result : boolean ) => {
333+ ffiTrace ( `>>WDONE seq=${ seq } ok=${ ! error && result } err=${ error ?. message ?? "-" } ` ) ;
292334 // Node-API completion callback: guard so a throw here (e.g. the stream's
293335 // own callback rejecting after teardown) can't escape across the FFI
294336 // boundary as an uncaught Node-API callback exception (DEP0168).
@@ -342,7 +384,9 @@ export class FfiRuntimeHost {
342384 bytesPtr ,
343385 koffi . array ( "uint8" , length , "Typed" )
344386 ) as Uint8Array ;
345- this . inboundQueue . push ( Buffer . from ( bytes ) ) ;
387+ const frame = Buffer . from ( bytes ) ;
388+ ffiTrace ( `<<R len=${ length } ${ frameTag ( frame ) } ` ) ;
389+ this . inboundQueue . push ( frame ) ;
346390 if ( ! this . drainScheduled ) {
347391 this . drainScheduled = true ;
348392 setImmediate ( ( ) => this . drainInbound ( ) ) ;
@@ -363,6 +407,7 @@ export class FfiRuntimeHost {
363407 }
364408 let frame : Buffer | undefined ;
365409 while ( ( frame = this . inboundQueue . shift ( ) ) !== undefined ) {
410+ ffiTrace ( `<<DELIVER len=${ frame . length } ${ frameTag ( frame ) } ` ) ;
366411 this . receiveStream . write ( frame ) ;
367412 }
368413 }
0 commit comments