@@ -339,6 +339,13 @@ it.effect.each(
339339 if ( delivery !== null ) {
340340 yield * Deferred . succeed ( rejectSteer , undefined ) ;
341341 yield * Fiber . join ( delivery ) ;
342+ const requeued = yield * orchestrator . getThreadProjection ( threadId ) ;
343+ const item = requeued . turnItems . find (
344+ ( item ) => item . type === "user_message" && item . messageId === messageId ,
345+ ) ;
346+ assert . equal ( item ?. status , "pending" ) ;
347+ assert . isNull ( item ?. startedAt ) ;
348+ assert . isNull ( item ?. completedAt ) ;
342349 }
343350 if ( timing === "before dispatch" ) yield * dispatchSteer ;
344351 yield * worker . drain ( ) ;
@@ -391,6 +398,14 @@ it.effect.each(
391398 ] ) ;
392399 assert . equal ( steerCalls , timing === "during delivery" ? 1 : 0 ) ;
393400 const final = yield * orchestrator . getThreadProjection ( threadId ) ;
401+ if ( ! mailbox ) {
402+ assert . equal (
403+ final . turnItems . find (
404+ ( item ) => item . type === "user_message" && item . messageId === messageId ,
405+ ) ?. status ,
406+ "completed" ,
407+ ) ;
408+ }
394409 assert . equal ( final . messages . filter ( ( message ) => message . id === messageId ) . length , 1 ) ;
395410 assert . equal (
396411 final . messages . find ( ( message ) => message . id === messageId ) ?. runId ,
@@ -669,6 +684,78 @@ it.effect("sends a selected held message once and preserves the remaining queue
669684 ) ,
670685) ;
671686
687+ it . effect ( "force sends multiple queued messages and can force send a requeued message again" , ( ) =>
688+ Effect . scoped (
689+ Effect . gen ( function * ( ) {
690+ const { started, steered, layer, startFirstTurn } = yield * nextTurnSelectionHarness (
691+ "multiple-promoted-messages" ,
692+ ) ;
693+ yield * Effect . gen ( function * ( ) {
694+ const orchestrator = yield * Orchestrator . OrchestratorV2 ;
695+ const worker = yield * EffectWorker . OrchestrationEffectWorkerV2 ;
696+ const threadId = yield * startFirstTurn ;
697+ const queue = ( text : string , commandId : string ) =>
698+ orchestrator . dispatch ( {
699+ type : "message.dispatch" ,
700+ commandId : CommandId . make ( commandId ) ,
701+ threadId,
702+ messageId : MessageId . make ( `message:${ text } ` ) ,
703+ text,
704+ attachments : [ ] ,
705+ dispatchMode : { type : "queue_after_active" } ,
706+ createdBy : "user" ,
707+ creationSource : "web" ,
708+ } ) ;
709+ for ( const text of [ "one" , "two" , "remaining" ] ) yield * queue ( text , `queue-${ text } ` ) ;
710+ const promote = Effect . fn ( "test.promote" ) ( function * ( text : string , commandId : string ) {
711+ const projection = yield * orchestrator . getThreadProjection ( threadId ) ;
712+ const queued = projection . runs . find (
713+ ( run ) =>
714+ run . status === "queued" && run . userMessageId === MessageId . make ( `message:${ text } ` ) ,
715+ ) ! ;
716+ yield * orchestrator . dispatch ( {
717+ type : "queued-message.promote-to-steer" ,
718+ commandId : CommandId . make ( commandId ) ,
719+ threadId,
720+ queuedRunId : queued . id ,
721+ targetRunId : started [ 0 ] ! . runId ,
722+ } ) ;
723+ } ) ;
724+ yield * promote ( "one" , "promote-one" ) ;
725+ yield * promote ( "two" , "promote-two" ) ;
726+ yield * worker . drain ( ) ;
727+ assert . deepEqual ( steered , [ "one" , "two" ] ) ;
728+
729+ // The completion-race fallback redispatches the same message into the queue.
730+ yield * queue ( "one" , "requeue-one" ) ;
731+ const requeued = yield * orchestrator . getThreadProjection ( threadId ) ;
732+ assert . equal (
733+ requeued . turnItems . find (
734+ ( item ) => item . type === "user_message" && item . messageId === "message:one" ,
735+ ) ?. status ,
736+ "pending" ,
737+ ) ;
738+ yield * promote ( "one" , "promote-one-again" ) ;
739+ yield * worker . drain ( ) ;
740+ assert . deepEqual ( steered , [ "one" , "two" , "one" ] ) ;
741+ assert . lengthOf ( started , 1 ) ;
742+ const final = yield * orchestrator . getThreadProjection ( threadId ) ;
743+ assert . deepEqual (
744+ final . runs . filter ( ( run ) => run . status === "queued" ) . map ( ( run ) => run . userMessageId ) ,
745+ [ MessageId . make ( "message:remaining" ) ] ,
746+ ) ;
747+ for ( const text of [ "one" , "two" ] ) {
748+ const items = final . turnItems . filter (
749+ ( item ) => item . type === "user_message" && item . messageId === `message:${ text } ` ,
750+ ) ;
751+ assert . lengthOf ( items , 1 ) ;
752+ assert . equal ( items [ 0 ] ?. status , "completed" ) ;
753+ }
754+ } ) . pipe ( Effect . provide ( layer ) ) ;
755+ } ) ,
756+ ) ,
757+ ) ;
758+
672759it . effect ( "keeps a message queued when send now races with a running turn" , ( ) =>
673760 Effect . scoped (
674761 Effect . gen ( function * ( ) {
0 commit comments