@@ -3675,6 +3675,261 @@ describe("v2 run and attempt history", () => {
36753675 "assistant-final-entry" ,
36763676 ] ) ;
36773677 } ) ;
3678+ it ( "folds a continued sub-agent with the turn that started it, not the run that resumed it" , ( ) => {
3679+ const run1 = RunId . make ( "run-1" ) ;
3680+ const run2 = RunId . make ( "run-2" ) ;
3681+ // Shaped like the stored data: the sub-agent item kept the ordinal it
3682+ // got in turn 1, but continuing it in turn 2 reassigned its runId to
3683+ // run 2 (CodexAdapterV2 emits the continued item with the parent's
3684+ // current run; TurnItemPositionStore.allocate never moves it).
3685+ const timelineEntries = [
3686+ {
3687+ id : "prompt-1" ,
3688+ kind : "message" as const ,
3689+ createdAt : "2026-01-01T00:00:00Z" ,
3690+ message : {
3691+ id : MessageId . make ( "prompt-1" ) ,
3692+ role : "user" as const ,
3693+ text : "Spawn a subagent" ,
3694+ runId : run1 ,
3695+ createdAt : "2026-01-01T00:00:00Z" ,
3696+ updatedAt : "2026-01-01T00:00:00Z" ,
3697+ streaming : false ,
3698+ } ,
3699+ } ,
3700+ {
3701+ id : "continued-subagent" ,
3702+ kind : "event" as const ,
3703+ createdAt : "2026-01-01T00:00:03Z" ,
3704+ projectedItem : {
3705+ item : {
3706+ type : "subagent" ,
3707+ runId : run2 ,
3708+ } ,
3709+ } ,
3710+ } as never ,
3711+ {
3712+ id : "answer-1" ,
3713+ kind : "message" as const ,
3714+ createdAt : "2026-01-01T00:00:20Z" ,
3715+ message : {
3716+ id : MessageId . make ( "answer-1" ) ,
3717+ role : "assistant" as const ,
3718+ text : "Subagent started." ,
3719+ runId : run1 ,
3720+ createdAt : "2026-01-01T00:00:20Z" ,
3721+ updatedAt : "2026-01-01T00:00:22Z" ,
3722+ streaming : false ,
3723+ } ,
3724+ } ,
3725+ {
3726+ id : "prompt-2" ,
3727+ kind : "message" as const ,
3728+ createdAt : "2026-01-01T00:01:00Z" ,
3729+ message : {
3730+ id : MessageId . make ( "prompt-2" ) ,
3731+ role : "user" as const ,
3732+ text : "Continue the subagent" ,
3733+ runId : run2 ,
3734+ createdAt : "2026-01-01T00:01:00Z" ,
3735+ updatedAt : "2026-01-01T00:01:00Z" ,
3736+ streaming : false ,
3737+ } ,
3738+ } ,
3739+ {
3740+ id : "work-2" ,
3741+ kind : "work" as const ,
3742+ createdAt : "2026-01-01T00:01:05Z" ,
3743+ entry : {
3744+ id : "work-2" ,
3745+ runId : run2 ,
3746+ createdAt : "2026-01-01T00:01:05Z" ,
3747+ label : "Running command" ,
3748+ tone : "tool" as const ,
3749+ toolLifecycleStatus : "completed" as const ,
3750+ } ,
3751+ } ,
3752+ {
3753+ id : "answer-2" ,
3754+ kind : "message" as const ,
3755+ createdAt : "2026-01-01T00:01:20Z" ,
3756+ message : {
3757+ id : MessageId . make ( "answer-2" ) ,
3758+ role : "assistant" as const ,
3759+ text : "Done." ,
3760+ runId : run2 ,
3761+ createdAt : "2026-01-01T00:01:20Z" ,
3762+ updatedAt : "2026-01-01T00:01:22Z" ,
3763+ streaming : false ,
3764+ } ,
3765+ } ,
3766+ ] ;
3767+
3768+ const rows = deriveMessagesTimelineRows ( {
3769+ timelineEntries,
3770+ isWorking : false ,
3771+ turnDiffSummaries : [ ] ,
3772+ supportsConversationRollback : false ,
3773+ } ) ;
3774+
3775+ // Each turn's fold sits between its own prompt and answer: the continued
3776+ // sub-agent folds with turn 1, and turn 2's work folds with turn 2.
3777+ expect ( rows . map ( ( row ) => row . id ) ) . toEqual ( [
3778+ "prompt-1" ,
3779+ "turn-fold:run-1" ,
3780+ "answer-1" ,
3781+ "prompt-2" ,
3782+ "turn-fold:run-2" ,
3783+ "answer-2" ,
3784+ ] ) ;
3785+ expect ( rows . filter ( ( row ) => row . kind === "turn-fold" ) . map ( ( row ) => row . runId ) ) . toEqual ( [
3786+ run1 ,
3787+ run2 ,
3788+ ] ) ;
3789+ } ) ;
3790+ it ( "does not move the fold of an ordinary single-run turn" , ( ) => {
3791+ const run1 = RunId . make ( "run-1" ) ;
3792+ const timelineEntries = [
3793+ {
3794+ id : "prompt-1" ,
3795+ kind : "message" as const ,
3796+ createdAt : "2026-01-01T00:00:00Z" ,
3797+ message : {
3798+ id : MessageId . make ( "prompt-1" ) ,
3799+ role : "user" as const ,
3800+ text : "Build it" ,
3801+ runId : run1 ,
3802+ createdAt : "2026-01-01T00:00:00Z" ,
3803+ updatedAt : "2026-01-01T00:00:00Z" ,
3804+ streaming : false ,
3805+ } ,
3806+ } ,
3807+ {
3808+ id : "work-1" ,
3809+ kind : "work" as const ,
3810+ createdAt : "2026-01-01T00:00:05Z" ,
3811+ entry : {
3812+ id : "work-1" ,
3813+ runId : run1 ,
3814+ createdAt : "2026-01-01T00:00:05Z" ,
3815+ label : "Ran command" ,
3816+ tone : "tool" as const ,
3817+ toolLifecycleStatus : "completed" as const ,
3818+ } ,
3819+ } ,
3820+ {
3821+ id : "answer-1" ,
3822+ kind : "message" as const ,
3823+ createdAt : "2026-01-01T00:00:20Z" ,
3824+ message : {
3825+ id : MessageId . make ( "answer-1" ) ,
3826+ role : "assistant" as const ,
3827+ text : "Done." ,
3828+ runId : run1 ,
3829+ createdAt : "2026-01-01T00:00:20Z" ,
3830+ updatedAt : "2026-01-01T00:00:22Z" ,
3831+ streaming : false ,
3832+ } ,
3833+ } ,
3834+ ] ;
3835+
3836+ const rows = deriveMessagesTimelineRows ( {
3837+ timelineEntries,
3838+ isWorking : false ,
3839+ turnDiffSummaries : [ ] ,
3840+ supportsConversationRollback : false ,
3841+ } ) ;
3842+
3843+ expect ( rows . map ( ( row ) => row . id ) ) . toEqual ( [ "prompt-1" , "turn-fold:run-1" , "answer-1" ] ) ;
3844+ } ) ;
3845+ it ( "does not move the fold of a sub-agent started fresh by a later turn" , ( ) => {
3846+ const run1 = RunId . make ( "run-1" ) ;
3847+ const run2 = RunId . make ( "run-2" ) ;
3848+ const timelineEntries = [
3849+ {
3850+ id : "prompt-1" ,
3851+ kind : "message" as const ,
3852+ createdAt : "2026-01-01T00:00:00Z" ,
3853+ message : {
3854+ id : MessageId . make ( "prompt-1" ) ,
3855+ role : "user" as const ,
3856+ text : "First question" ,
3857+ runId : run1 ,
3858+ createdAt : "2026-01-01T00:00:00Z" ,
3859+ updatedAt : "2026-01-01T00:00:00Z" ,
3860+ streaming : false ,
3861+ } ,
3862+ } ,
3863+ {
3864+ id : "answer-1" ,
3865+ kind : "message" as const ,
3866+ createdAt : "2026-01-01T00:00:20Z" ,
3867+ message : {
3868+ id : MessageId . make ( "answer-1" ) ,
3869+ role : "assistant" as const ,
3870+ text : "First answer." ,
3871+ runId : run1 ,
3872+ createdAt : "2026-01-01T00:00:20Z" ,
3873+ updatedAt : "2026-01-01T00:00:22Z" ,
3874+ streaming : false ,
3875+ } ,
3876+ } ,
3877+ {
3878+ id : "prompt-2" ,
3879+ kind : "message" as const ,
3880+ createdAt : "2026-01-01T00:01:00Z" ,
3881+ message : {
3882+ id : MessageId . make ( "prompt-2" ) ,
3883+ role : "user" as const ,
3884+ text : "Spawn a subagent" ,
3885+ runId : run2 ,
3886+ createdAt : "2026-01-01T00:01:00Z" ,
3887+ updatedAt : "2026-01-01T00:01:00Z" ,
3888+ streaming : false ,
3889+ } ,
3890+ } ,
3891+ {
3892+ id : "fresh-subagent" ,
3893+ kind : "event" as const ,
3894+ createdAt : "2026-01-01T00:01:03Z" ,
3895+ projectedItem : {
3896+ item : {
3897+ type : "subagent" ,
3898+ runId : run2 ,
3899+ } ,
3900+ } ,
3901+ } as never ,
3902+ {
3903+ id : "answer-2" ,
3904+ kind : "message" as const ,
3905+ createdAt : "2026-01-01T00:01:20Z" ,
3906+ message : {
3907+ id : MessageId . make ( "answer-2" ) ,
3908+ role : "assistant" as const ,
3909+ text : "Done." ,
3910+ runId : run2 ,
3911+ createdAt : "2026-01-01T00:01:20Z" ,
3912+ updatedAt : "2026-01-01T00:01:22Z" ,
3913+ streaming : false ,
3914+ } ,
3915+ } ,
3916+ ] ;
3917+
3918+ const rows = deriveMessagesTimelineRows ( {
3919+ timelineEntries,
3920+ isWorking : false ,
3921+ turnDiffSummaries : [ ] ,
3922+ supportsConversationRollback : false ,
3923+ } ) ;
3924+
3925+ expect ( rows . map ( ( row ) => row . id ) ) . toEqual ( [
3926+ "prompt-1" ,
3927+ "answer-1" ,
3928+ "prompt-2" ,
3929+ "turn-fold:run-2" ,
3930+ "answer-2" ,
3931+ ] ) ;
3932+ } ) ;
36783933 it ( "collapses only output from a superseded V2 attempt within the active logical run" , ( ) => {
36793934 const runId = "run-steered" as never ;
36803935 const supersededAttemptId = "attempt-1" as never ;
0 commit comments