@@ -651,63 +651,97 @@ const nextTurnSelectionHarness = Effect.fn("nextTurnSelectionHarness")(function*
651651 return { cwd, events, started, steered, interrupted, layer, startFirstTurn } ;
652652} ) ;
653653
654- it . effect ( "steers a delegated task question notice into the running parent turn" , ( ) =>
655- Effect . scoped (
656- Effect . gen ( function * ( ) {
657- const { started, steered, layer, startFirstTurn } = yield * nextTurnSelectionHarness (
658- "delegated-question-steer" ,
659- ) ;
660- yield * Effect . gen ( function * ( ) {
661- const orchestrator = yield * Orchestrator . OrchestratorV2 ;
662- const worker = yield * EffectWorker . OrchestrationEffectWorkerV2 ;
663- const threadId = yield * startFirstTurn ;
664- const { thread } = yield * orchestrator . getThreadProjection ( threadId ) ;
665- yield * ( yield * EventSink . EventSinkV2 ) . write ( {
666- events : [
667- {
668- id : EventId . make ( "event:composer-selection" ) ,
669- type : "thread.model-selection-updated" ,
670- threadId,
671- providerInstanceId : instanceId ,
672- occurredAt : yield * DateTime . now ,
673- payload : { ...thread , modelSelection : composerSelection } ,
674- } ,
675- ] ,
676- } ) ;
677- const messageId = MessageId . make ( "message:delegated-task-question:child-question" ) ;
678- const notification = {
679- source : {
680- kind : "delegated_task" as const ,
681- taskIds : [ NodeId . make ( "task:question" ) ] ,
682- childThreadId : ThreadId . make ( "thread:question-child" ) ,
683- } ,
684- outcome : "updated" as const ,
685- summary : "Delegated task has a question" ,
686- } ;
687- yield * orchestrator . dispatch ( {
688- type : "message.dispatch" ,
689- commandId : CommandId . make ( "question-wake" ) ,
690- threadId,
691- messageId,
692- text : "Delegated task asked a question" ,
693- notification,
694- attachments : [ ] ,
695- dispatchMode : { type : "queue_after_active" } ,
696- createdBy : "agent" ,
697- creationSource : "server" ,
698- } ) ;
699- yield * worker . drain ( ) ;
700- const after = yield * orchestrator . getThreadProjection ( threadId ) ;
701- assert . deepEqual ( steered , [ "Delegated task asked a question" ] ) ;
702- assert . lengthOf ( started , 1 ) ;
703- assert . isFalse ( after . runs . some ( ( run ) => run . status === "queued" ) ) ;
704- const message = after . messages . find ( ( candidate ) => candidate . id === messageId ) ;
705- assert . equal ( message ?. runId , started [ 0 ] ?. runId ) ;
706- assert . deepEqual ( message ?. notification , notification ) ;
707- assert . deepEqual ( after . thread . modelSelection , composerSelection ) ;
708- } ) . pipe ( Effect . provide ( layer ) ) ;
709- } ) ,
710- ) ,
654+ it . effect . each ( [
655+ { notice : "delegated task question" , goal : null , steers : true } ,
656+ { notice : "background command" , goal : "active" , steers : true } ,
657+ { notice : "background command" , goal : null , steers : false } ,
658+ ] as const ) (
659+ "a $notice notice steers into the running turn: $steers (goal $goal)" ,
660+ ( { notice, goal, steers } ) =>
661+ Effect . scoped (
662+ Effect . gen ( function * ( ) {
663+ const { started, steered, layer, startFirstTurn } = yield * nextTurnSelectionHarness (
664+ `notice-steer-${ notice . replaceAll ( " " , "-" ) } -${ goal ?? "none" } ` ,
665+ ) ;
666+ yield * Effect . gen ( function * ( ) {
667+ const orchestrator = yield * Orchestrator . OrchestratorV2 ;
668+ const worker = yield * EffectWorker . OrchestrationEffectWorkerV2 ;
669+ const threadId = yield * startFirstTurn ;
670+ const { thread } = yield * orchestrator . getThreadProjection ( threadId ) ;
671+ yield * ( yield * EventSink . EventSinkV2 ) . write ( {
672+ events : [
673+ {
674+ id : EventId . make ( "event:composer-selection" ) ,
675+ type : "thread.model-selection-updated" ,
676+ threadId,
677+ providerInstanceId : instanceId ,
678+ occurredAt : yield * DateTime . now ,
679+ payload : { ...thread , modelSelection : composerSelection } ,
680+ } ,
681+ ] ,
682+ } ) ;
683+ if ( goal !== null ) {
684+ const providerThread = ( yield * orchestrator . getThreadProjection ( threadId ) )
685+ . providerThreads [ 0 ] ! ;
686+ yield * ( yield * EventSink . EventSinkV2 ) . write ( {
687+ events : [
688+ {
689+ id : EventId . make ( "event:goal-active" ) ,
690+ type : "provider-thread.updated" ,
691+ threadId,
692+ occurredAt : yield * DateTime . now ,
693+ payload : { ...providerThread , goal : { objective : "Ship it" , status : goal } } ,
694+ } ,
695+ ] ,
696+ } ) ;
697+ }
698+ const messageId = MessageId . make ( "message:notice" ) ;
699+ const notification =
700+ notice === "delegated task question"
701+ ? {
702+ source : {
703+ kind : "delegated_task" as const ,
704+ taskIds : [ NodeId . make ( "task:question" ) ] ,
705+ childThreadId : ThreadId . make ( "thread:question-child" ) ,
706+ } ,
707+ outcome : "updated" as const ,
708+ summary : "Delegated task has a question" ,
709+ }
710+ : {
711+ source : { kind : "command" as const } ,
712+ outcome : "completed" as const ,
713+ summary : "Background command finished" ,
714+ } ;
715+ yield * orchestrator . dispatch ( {
716+ type : "message.dispatch" ,
717+ commandId : CommandId . make ( "notice-wake" ) ,
718+ threadId,
719+ messageId,
720+ text : `${ notification . summary } \nexit 0: built 12 files` ,
721+ notification,
722+ attachments : [ ] ,
723+ dispatchMode : { type : "queue_after_active" } ,
724+ createdBy : "agent" ,
725+ creationSource : "server" ,
726+ } ) ;
727+ yield * worker . drain ( ) ;
728+ const after = yield * orchestrator . getThreadProjection ( threadId ) ;
729+ assert . deepEqual (
730+ steered ,
731+ steers ? [ `${ notification . summary } \nexit 0: built 12 files` ] : [ ] ,
732+ ) ;
733+ assert . lengthOf ( started , 1 ) ;
734+ assert . equal (
735+ after . runs . some ( ( run ) => run . status === "queued" ) ,
736+ ! steers ,
737+ ) ;
738+ const message = after . messages . find ( ( candidate ) => candidate . id === messageId ) ;
739+ assert . deepEqual ( message ?. notification , notification ) ;
740+ if ( steers ) assert . equal ( message ?. runId , started [ 0 ] ?. runId ) ;
741+ assert . deepEqual ( after . thread . modelSelection , composerSelection ) ;
742+ } ) . pipe ( Effect . provide ( layer ) ) ;
743+ } ) ,
744+ ) ,
711745) ;
712746
713747it . effect ( "sends a selected held message once and preserves the remaining queue order" , ( ) =>
0 commit comments