@@ -519,3 +519,117 @@ describe("installSessionToolResultGuard", () => {
519519 expect ( syntheticForError ) . toHaveLength ( 0 ) ;
520520 } ) ;
521521} ) ;
522+
523+ describe ( "concurrent append race condition" , ( ) => {
524+ it ( "does not synthesize when toolResult arrives concurrently with next assistant message" , async ( ) => {
525+ // Simulate the exact race: tool_calls appended, then toolResult and a
526+ // completion message are dispatched concurrently (both microtasks queued).
527+ const sm = SessionManager . inMemory ( ) ;
528+ installSessionToolResultGuard ( sm ) ;
529+
530+ sm . appendMessage (
531+ asAppendMessage ( {
532+ role : "assistant" ,
533+ content : [ { type : "toolCall" , id : "race_1" , name : "read" , arguments : { } } ] ,
534+ } ) ,
535+ ) ;
536+
537+ // Fire both concurrently — do NOT await between them
538+ const p1 = Promise . resolve ( ) . then ( ( ) =>
539+ sm . appendMessage (
540+ asAppendMessage ( {
541+ role : "toolResult" ,
542+ toolCallId : "race_1" ,
543+ content : [ { type : "text" , text : "real result" } ] ,
544+ isError : false ,
545+ } ) ,
546+ ) ,
547+ ) ;
548+ const p2 = Promise . resolve ( ) . then ( ( ) =>
549+ sm . appendMessage (
550+ asAppendMessage ( {
551+ role : "assistant" ,
552+ content : [ { type : "text" , text : "completion" } ] ,
553+ stopReason : "endTurn" ,
554+ } ) ,
555+ ) ,
556+ ) ;
557+
558+ await Promise . all ( [ p1 , p2 ] ) ;
559+
560+ const messages = expectPersistedRoles ( sm , [ "assistant" , "toolResult" , "assistant" ] ) ;
561+ // The toolResult must be the real one, not synthetic
562+ const tr = messages [ 1 ] as { isError ?: boolean ; content ?: Array < { text ?: string } > } ;
563+ expect ( tr . isError ) . toBe ( false ) ;
564+ expect ( tr . content ?. [ 0 ] ?. text ) . toBe ( "real result" ) ;
565+ } ) ;
566+
567+ it ( "serializes multiple concurrent appends without duplicating messages" , async ( ) => {
568+ const sm = SessionManager . inMemory ( ) ;
569+ installSessionToolResultGuard ( sm ) ;
570+
571+ sm . appendMessage (
572+ asAppendMessage ( {
573+ role : "assistant" ,
574+ content : [
575+ { type : "toolCall" , id : "c1" , name : "a" , arguments : { } } ,
576+ { type : "toolCall" , id : "c2" , name : "b" , arguments : { } } ,
577+ ] ,
578+ } ) ,
579+ ) ;
580+
581+ // Fire all results concurrently
582+ await Promise . all ( [
583+ Promise . resolve ( ) . then ( ( ) =>
584+ sm . appendMessage (
585+ asAppendMessage ( {
586+ role : "toolResult" ,
587+ toolCallId : "c1" ,
588+ content : [ { type : "text" , text : "r1" } ] ,
589+ isError : false ,
590+ } ) ,
591+ ) ,
592+ ) ,
593+ Promise . resolve ( ) . then ( ( ) =>
594+ sm . appendMessage (
595+ asAppendMessage ( {
596+ role : "toolResult" ,
597+ toolCallId : "c2" ,
598+ content : [ { type : "text" , text : "r2" } ] ,
599+ isError : false ,
600+ } ) ,
601+ ) ,
602+ ) ,
603+ ] ) ;
604+
605+ const messages = expectPersistedRoles ( sm , [ "assistant" , "toolResult" , "toolResult" ] ) ;
606+ // Both results must be the real ones, not synthetic
607+ const results = messages . slice ( 1 ) as Array < { isError ?: boolean } > ;
608+ expect ( results . every ( ( r ) => r . isError === false ) ) . toBe ( true ) ;
609+ } ) ;
610+
611+ it ( "getPendingIds() returns empty after concurrent resolution" , async ( ) => {
612+ const sm = SessionManager . inMemory ( ) ;
613+ const guard = installSessionToolResultGuard ( sm ) ;
614+
615+ sm . appendMessage (
616+ asAppendMessage ( {
617+ role : "assistant" ,
618+ content : [ { type : "toolCall" , id : "drain_1" , name : "x" , arguments : { } } ] ,
619+ } ) ,
620+ ) ;
621+
622+ await Promise . resolve ( ) . then ( ( ) =>
623+ sm . appendMessage (
624+ asAppendMessage ( {
625+ role : "toolResult" ,
626+ toolCallId : "drain_1" ,
627+ content : [ { type : "text" , text : "ok" } ] ,
628+ isError : false ,
629+ } ) ,
630+ ) ,
631+ ) ;
632+
633+ expect ( guard . getPendingIds ( ) ) . toEqual ( [ ] ) ;
634+ } ) ;
635+ } ) ;
0 commit comments