@@ -30,7 +30,15 @@ type SupervisorErrorCode =
3030 * never sees raw upstream text.
3131 */
3232function emitError ( code : SupervisorErrorCode , detail : unknown ) : AgentEvent {
33- logger . warn ( "supervisor-api error code=%s detail=%O" , code , detail ) ;
33+ // Summarise at `warn` (CWE-532: never dump the full upstream payload to
34+ // the default log level); the verbose object is only available via
35+ // `DEBUG=appkit:agents:supervisor-api`.
36+ logger . warn (
37+ "supervisor-api error code=%s detail=%s" ,
38+ code ,
39+ summariseErrorPayload ( detail ) ,
40+ ) ;
41+ logger . debug ( "supervisor-api error code=%s detail=%O" , code , detail ) ;
3442 return {
3543 type : "status" ,
3644 status : "error" ,
@@ -353,51 +361,64 @@ export class SupervisorApiAdapter implements AgentAdapter {
353361 }
354362 | undefined ;
355363
356- for await ( const { event, data } of readSseEvents ( stream , signal ) ) {
357- if ( data === "[DONE]" ) continue ;
358-
359- let parsed : Record < string , unknown > ;
360- try {
361- parsed = JSON . parse ( data ) ;
362- } catch ( err ) {
363- logger . debug (
364- "Failed to parse SSE data line: %s (%O)" ,
365- data . slice ( 0 , 200 ) ,
366- err ,
367- ) ;
368- continue ;
369- }
364+ // `readSseEvents` throws on transport errors and on the DoS caps
365+ // (maxLineChars / maxBufferChars). Without this guard the rejection
366+ // propagates out of `run()` and tears down the request. Treat a
367+ // consumer-initiated abort as a clean stop; everything else becomes a
368+ // sanitised terminal `transport` error.
369+ try {
370+ for await ( const { event, data } of readSseEvents ( stream , signal ) ) {
371+ if ( data === "[DONE]" ) continue ;
372+
373+ let parsed : Record < string , unknown > ;
374+ try {
375+ parsed = JSON . parse ( data ) ;
376+ } catch ( err ) {
377+ logger . debug (
378+ "Failed to parse SSE data line: %s (%O)" ,
379+ data . slice ( 0 , 200 ) ,
380+ err ,
381+ ) ;
382+ continue ;
383+ }
370384
371- const eventType = event || ( parsed . type as string ) || "" ;
372- eventCounts . set ( eventType , ( eventCounts . get ( eventType ) ?? 0 ) + 1 ) ;
373-
374- // `response.completed` is held back until after the loop so we can
375- // synthesise a `message_delta` from `response.output[]` when the
376- // stream produced no incremental deltas (intermittent SA behaviour).
377- // Emitting `complete` first would let UIs finalise the turn before the
378- // recovered text arrives.
379- if ( eventType === "response.completed" ) {
380- lastCompleted = parsed . response as typeof lastCompleted ;
381- continue ;
382- }
385+ const eventType =
386+ event || ( typeof parsed . type === "string" ? parsed . type : "" ) ;
387+ eventCounts . set ( eventType , ( eventCounts . get ( eventType ) ?? 0 ) + 1 ) ;
388+
389+ // `response.completed` is held back until after the loop so we can
390+ // synthesise a `message_delta` from `response.output[]` when the
391+ // stream produced no incremental deltas (intermittent SA behaviour).
392+ // Emitting `complete` first would let UIs finalise the turn before the
393+ // recovered text arrives.
394+ if ( eventType === "response.completed" ) {
395+ lastCompleted = parsed . response as typeof lastCompleted ;
396+ continue ;
397+ }
383398
384- const out = mapEvent ( eventType , parsed , streamedItemIds ) ;
385- if ( out ) {
386- if ( out . type === "message_delta" ) receivedAnyDelta = true ;
387- yield out ;
388- if ( out . type === "status" && out . status === "error" ) {
389- terminated = true ;
390- break ;
399+ const out = mapEvent ( eventType , parsed , streamedItemIds ) ;
400+ if ( out ) {
401+ if ( out . type === "message_delta" ) receivedAnyDelta = true ;
402+ yield out ;
403+ if ( out . type === "status" && out . status === "error" ) {
404+ terminated = true ;
405+ break ;
406+ }
391407 }
392408 }
409+ } catch ( err ) {
410+ if ( signal ?. aborted ) return ;
411+ yield emitError ( "transport" , err ) ;
412+ return ;
393413 }
394414
395415 if ( signal ?. aborted ) return ;
396416
397417 if ( eventCounts . size === 0 ) {
398- logger . warn (
399- "Supervisor API stream closed without emitting any SSE events." ,
400- ) ;
418+ // A stream that closes without a single event leaves the consumer
419+ // stuck in `running`. Surface a terminal `transport` error so the
420+ // turn ends.
421+ yield emitError ( "transport" , "stream closed without events" ) ;
401422 return ;
402423 }
403424
@@ -419,14 +440,15 @@ export class SupervisorApiAdapter implements AgentAdapter {
419440
420441 if ( eventCounts . has ( "response.completed" ) ) {
421442 // SA sometimes signals a failed turn via `response.completed` with a
422- // nested `status: "failed"` (or a populated `error`/`incomplete_details`)
423- // rather than emitting `response.failed`. Without this gate the
424- // adapter would silently yield `complete` on a server-side failure.
425- if (
426- lastCompleted ?. status === "failed" ||
427- lastCompleted ?. error != null ||
428- lastCompleted ?. incomplete_details != null
429- ) {
443+ // nested `status: "failed"` (or a populated `error`) rather than
444+ // emitting `response.failed`. Without this gate the adapter would
445+ // silently yield `complete` on a server-side failure.
446+ //
447+ // `incomplete_details` on its own is NOT fatal: a benign
448+ // `max_output_tokens` truncation populates it while still producing
449+ // usable partial output. In that case we fall through to `complete`
450+ // and let the recovered text above stand as the turn result.
451+ if ( lastCompleted ?. status === "failed" || lastCompleted ?. error != null ) {
430452 yield emitError ( "upstream_failed" , {
431453 status : lastCompleted ?. status ,
432454 error : lastCompleted ?. error ,
@@ -539,9 +561,13 @@ function mapEvent(
539561 // than we care to map.
540562 switch ( eventType as ResponseStreamEvent [ "type" ] ) {
541563 case "response.output_text.delta" : {
542- const itemId = data . item_id as string | undefined ;
564+ const itemId =
565+ typeof data . item_id === "string" ? data . item_id : undefined ;
543566 if ( itemId ) streamedItemIds . add ( itemId ) ;
544- return { type : "message_delta" , content : ( data . delta as string ) ?? "" } ;
567+ return {
568+ type : "message_delta" ,
569+ content : typeof data . delta === "string" ? data . delta : "" ,
570+ } ;
545571 }
546572
547573 // `response.completed` is intentionally absent: `streamResponse` holds
0 commit comments