From 196964ef10db326047c3e71fc568693cbd7c58a8 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Thu, 10 Sep 2026 17:59:45 +0000 Subject: [PATCH] Preserve root turn attribution in turn-start events (#44611) ## Why Persisted turn-start events lack the originating root turn ID, so they cannot directly associate child turns with the root turn that initiated them. ## What changed - Add optional `root_turn_id` to `TurnStartedEvent`. Use the inherited root turn ID when available, otherwise the turn's own ID. - Centralize turn-start emission for regular turns, shell commands, and manual compaction so they record attribution consistently. - Carry attribution into `ThreadHistoryTurnChange` when processing turn-start events and retain it through completion in the history builder. - Keep older records compatible by defaulting missing attribution to `None`. ## Testing Add coverage for root and child turn attribution, persistence when tool collisions fail a turn before sampling, history change propagation, and deserialization of older turn-start records. GitOrigin-RevId: 54a80b828ca7d1c4c19f13d468c01d052e1b9130 --- .../app-server-exports-stable.json.zst | Bin 149929 -> 149982 bytes .../src/protocol/item_builders_tests.rs | 1 + .../src/protocol/thread_history.rs | 48 ++++++++++++++- .../src/protocol/thread_history_projection.rs | 3 + .../thread_history_projection_tests.rs | 8 +++ .../app-server/src/bespoke_event_handling.rs | 3 + .../thread_processor_tests.rs | 1 + .../app-server/src/turn_cost_worker_tests.rs | 1 + .../app-server/tests/suite/v2/thread_fork.rs | 3 + .../app-server/tests/suite/v2/thread_read.rs | 1 + .../tests/suite/v2/thread_resume.rs | 5 ++ .../tests/suite/v2/thread_timeline.rs | 1 + codex-rs/core/src/agent/control_tests.rs | 1 + codex-rs/core/src/compact.rs | 10 +--- codex-rs/core/src/compact_remote_v2.rs | 10 +--- codex-rs/core/src/compact_token_budget.rs | 11 +--- codex-rs/core/src/realtime_history_tests.rs | 2 + codex-rs/core/src/session/mod.rs | 19 ++++++ .../session/rollout_reconstruction_tests.rs | 28 +++++++++ codex-rs/core/src/session/tests.rs | 10 ++++ codex-rs/core/src/tasks/regular.rs | 11 +--- codex-rs/core/src/tasks/user_shell.rs | 10 +--- codex-rs/core/src/thread_manager_tests.rs | 2 + .../src/thread_rollout_truncation_tests.rs | 1 + codex-rs/core/tests/suite/agent_execution.rs | 56 ++++++++++++++++++ codex-rs/core/tests/suite/resume_warning.rs | 1 + codex-rs/core/tests/suite/tools.rs | 32 +++++++--- .../src/sessions/export.rs | 1 + codex-rs/protocol/src/protocol.rs | 14 +++++ .../rollout/src/persistence_metrics_tests.rs | 1 + codex-rs/thread-store/src/local/mod.rs | 1 + .../src/local/model_context_tests.rs | 1 + .../src/local/revert_thread_tests.rs | 1 + .../local/rollout_migration/canonicalizer.rs | 1 + .../src/local/rollout_migration_tests.rs | 1 + .../thread_history_materialization_tests.rs | 1 + .../thread-store/src/thread_metadata_sync.rs | 1 + codex-rs/tui/src/app/tests.rs | 1 + .../app/tests/pagination_completion_tests.rs | 1 + .../app/tests/session_lifecycle_requests.rs | 3 + 40 files changed, 252 insertions(+), 55 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 414537133bdd2481df12748244eb96dc428d5ff1..53d7404eae1d45c45756ca792afe70fbead808bc 100644 GIT binary patch delta 19639 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 delta 19586 zcmV(zK<2;RlL@Jl34pW#w*7z1vH*snU@LHI(gFPc|NsB1>I?ceLN+CV;d`tTJ}I=5gs7V|Z=D$7M1&=DQayXdh?LI5~g}X4E5oIUBY)#x>5NWD(_$Pp7+=QB;tX5an$4RsYpX8`rrUS zlb|6KhH;U>Ir*-~Tk?NU%kD~Q4I+d{rO%MlVg-@J6Y?P%O{JC@DJ!vI3JG2EUU$Z* z`QI;9Nw>SPZ()S)@pm32wSofCB}H5dasFh_Q0kSti*j|4NR{){kr{>De%mzKnO3iZ zm80~^h27Y-piT>RBd2thG$q#Q-~=}IHS7;qyByG$wT(c$!u&Rx4N5G>cTCoyd6$Nm zsMFn)O_CHE12Z5HkU+sf#6uHlFc=F5VHA+*4TX|L0B~!akdL=6V@KW{+y1Rp<5QXoSZYB1)nwlP*1xDGw_yt6#m| zne`2uh$e@dtyP@#<`1W@iHEIi6zvHJTKQpsO3H{|izdpT1c%qOA25Gqk*#(et}y!P zuc+j@Z0v}DDc-#fi!)L{uFNu4n+dw>cYkAc9@l{MU)Z)9Lc0mI5(~g&1G;<{BG!&} z#$`+0XEEVmOYyoO2VJ#yf%(o9v;9;P8MJvowIQ=g<>+54DM+;xhH+dj^oT@e_ZfpkR9t4PLR4$0p{Bm7kNcn z*SBm8*GIL}nYiL4!lVK@+dlYht;fm*eo84zgId&o#c#yOwcnuTFVo-w&U}GPQ37AF zazjzN@Zu%}jWAK#0veBLe1Okq{YOhcjzCC&(FmL4ixf)K4Qzkf@rI)9f`oMvlp44^ zZj{a$Mv+S_Z=hE}PG|}38`kXk-KmuYk*=G{KsfyS z)gSgpx0X^Hz`1{s#AnYa)S#1n$K;|CR1z55rKyEv(~&d&NDY#QrVS*0eUbvOq8VQs zj`P9%J}vL^FPREDApGyzM~q+_Y&l5TM9BcK0MPAKV?m}D?eS{o^HkxR-W2g8#_I&4 zx<%p5(mltPvwH1d#aLOjo%^WFD&SKk5$7;e21D%jZjXPwaWU(7B^4jl{7Vk3TE6kU z!^p-FH7?zVfXx^tg%k}M`JuWisqCMqmKc76(c^%D@WyP$EnWE+C^S~oeeAjctPEK< zaRZM-3qy?6`*Lj{sDP2SGgc^YuT1*av}=TrUknB#JaW%V>UR$FC@aw9!g=-a6d+#* z0N<`uJF0*BOLG>FidMAb?Q@A1uf(Y8N|^2I5S0-p=B4OEr2 z^Lr=5>el4;Wwf%^)N(s+g~h#_yN0B{hyTIUd%hLxj}t=8{@Q6#I=n&D^bgbdlM>b2 z4%F%=jpEVx{gGJb&11|>cT0Dt+QWOLG^|J#6On)X8m($44rxVx1h;FP42f>}2>w|g)%W@~WCgCQH)ZXlR#P&iHlABZ&q z#O!~4;u=!67TC7c^cSFcybArg8?{}#51Ig@)`B|&u-l%#Ha}XP4=0i=J@A%@-7sh=f&4_gA`4t6HN0PFazD&4J|dIHej3+ak}k`pRJfchKXXakPX z%ICeyQ_fj4MN|bu?Fy~p3y$6N@7pg!z>wf%IUA^3f92~g-C+28BLYf2Z#cE}zXxqYLWRAf=4t4ysIrgq5?TT*;K6$3at2Q7oC1!2uO!h?;X=mPcDna&kGvkiY7b8x)I zvHY8tz~A*6k=Gw?RO<-+S1YY39FL*%{ob8T|Ayq(5oJU9bzGrqUn%qW6|V)d0=P4` z@s(eF$+&In5=l30JSi-!27sB`;lZ@y{y+SPdejX}jXKC=c>S5kspHxr)OR46->8Ly z-R0P+oEW{OkxD3(r0a!+W9wg$W2kKE2b_A zxph!5Yw1(Zm}?G@YL_VS8o_WW{rDamnX`Ppt`p$fk-$-|OS;oOx1vK{TL8fRqWP9pZY7#92UPsQHhETNF2MT@+h(V#)@U=Nqkth zI>jpvh-9+N#xpJ5-tp|5NjWfSEI-zo)rCGYMr2iUUe9%iVh5+;{f8i-Zp)V|3V6J5 zeh7aVwSnL3Fh_QzdX|4Sc!VUnY9^9)jIlsj^tI*Gqj0wI6$s1vEfuRajfrNki} zEF^X!WKET(zq5#RKq92&swvT!ny~|Oog0m9@0l?WU$ME0td_j}V`}2>8~ORgr5;7U z=2jJgarZ;n#bkwO#R^#hu&4iOiM`yJa8DR$N5$mWD!-^U^iT!%xmz*8%TItQ#Mp=MQcn+czt}@3M6WG! z;~p)5jK^^f*5XepGTIw;2~;an!qe11b^$x30)HIG&evr zfTtu%s|Z=>Tq84l7pq(K1vW^~C60Pa);+Rw2Nl&6!q9?&`nsZ>}d;K|K3*ILY}Y{7=G>#kZI z#@h9wLk+lJ!r zfWZl!hD`Z&&eD$t1(kmxgYF2LO@O+_Q2A;5A)9&==czzCf}h_uBPp&^ zXQzL+iBSQH!1MrE^FN=7ZF)0(FSS7mN9Fx}igOo2t}oegkFQ*%EO%UB-?L@I&! zXYk_a3V#|ue&NeBgv_0m5;YzwN7Sc=-La(&b)DkcuYj1#QDQzexe`m&4mVgZCPGX{ z=KFR|jygovP-exssuol4h3fy0i@+x*Me2W+c2SvT?ydPwUg#Zr&uNC!3QXqA!0D zgRw962-4vHfY~HDQV}-VXQ}``#g`ImN9xHrysh@ZiF|OwAGeUVoh1M>FGEms93J0O zP7B$D8+c$wR)ssK8^;0N5jaUsDo5ayV!VY;%ol-$sRSM3Mc_nKAUN&kn{R>eH?=MV z`sSc3leA^S0@&83&j4_J}w7{PFpl#(;BF@aTf-TVCf2=_HGU@_NhLMHX~o7l)7-^mmu< z+*R*;5iRtEI%1>~bF?Vw7bW|^f^URXXUf66TdMf~DHIrjX%IDuf@jEF>Cc%MaT&ug z_89bN=@7AnXK!yOXo7&rIYECj!7?H}-#Y59YI=UcsTf9-%T-M}1YFhhsL0lMzSa1R z;`tW+F1fU@QqAat=i3^${cb11DT^HgTV|g zi04Kf3oMFchcRII(kfuM9n+n%X&XXXB@_s0Jt2ZQdC0NU$!~=CmX?2}sd1SE*=lxn zifXSSO*&#cQw|7|IfHYU-1kSEu!TrNU!tsjpFbKv2@)+EMcqf^1vjX!R)rp{Hje=@ zX-J@)(ec#_8^v+~JcZYdYg)9r^N3^)3Y-^>3KcfxIc(&~1~;Xo`&$~{AH~d^uI8V{ z4l&7DQ6l+F7Z;ic!x4YYEH!qR&0t|10tzEBH}F3$mn%+*T9KHv1qu*oNp2W2jv944 zNF6fko}OqYr`ywP%}^l(s<9z~aVRdT`HQCQ@Ca896z@^TK0*bjhlA-?AfWmbie9k2 z11j;{ldBWUWy{vho~DbU;jPN2CV5ubeH0T~F!Ba-0#1XaC>MW#L!c^IpTvR0bxkKG zi?}1+Hdk-e8S9pt43j6ss+Sb**&c%&8F>nyOkoybq$RQ zF7Z{T_q+#ip6+&AuHpEN61m-cT~LG976x{^Jr`t-XBW;ggW3&ryM1Ese)4i|x9UhL zZLmplTPTXcY(jqt&ndTC#Svf@B+{bp=BjS}uIjp(DoHl};4 z4dja6Z<%4rn9yWb(pS1{jZkKojBEIeGfPg982C(NrV&L#MlFa$8*WtCbTm0F$plSK z+dgk{I*-FnPS3SV=`d*)<#@y+Z;7irL)U4x%Zb z)nP|ZM3q{e##0Q?GBSc;z|<^QAn1+-+XzFrn4#Ji&F8S_Rw013WyAoWx3h$m`d9m$eKQb?jc*Pap$F?ApG zHZ;YET$I9DwiXs>)1);VC4iAN)G<2Hylj}~HS!f`kan_SCjTpRX7C*DxA9K>njFw-{AsI zmvzyAXZRFD(LJ}lmJ`4FiV<12BKkA%7dITL!`iiGbUA)AXk`93dq#-Sjg=eG&7*(5 z$@)%y^bPoMnVUFN%UIuv4oHxS6rp3-aP0JZz0SK@XWTn;NO5;c78W$D635?Yi}7Xt zhgLTN0Y`5*4G6QWm%G1iy23ezx`f5n9an6l>~GbPEZrri`vuQ?-7!(SDYdU7EF(lq{;wR}rIF75(IZxcEpthe!Sj1faVdmZ;k)w{0yXdEK+ zIAWS_F^*yI;z4>uX&p^H%nlc60)wW?&T?+1x}>m07mF? zERZW5|NsAgs#)nqWN(?&NJ<+CB6cUacaq|Fw_M{RS`DbDc>5$Y+(Pun9A*G#0B8XH z8a~9^zEMom_Z#lJ_JMy}YF$>oRga=Ugb$(ib6+nLwl0?Wz6AgUzQ;dL+%57@d#uuA zp+G#T!=~B#GEZ&~Z4`r#wU}?uzqXzPy&?D0YoN@2tUVx1f!gC$Pt=Jpk)TA@*BPJC z>h#nWP?3#%g=-i1XPm%z`@NQz<#i3QGG6Cc*WpATIBctjv0i`FP5hchN>JV>iTC5I zu9<0;690u-Q@qVB3_eV-aRwibBQdV~l%c`p*ywoN9$`PO)u0rOUB{@s?u(kiC<;;K zo98-j=#%Yo)ybNc{A9>nxTf|G$$|ufuk}M9FtU~QXCP8QKoA*lW$GBqe7n-O)Ne(X zv(Qm+m^eg89>RZj_LAH%rjcq0i^pdB3yray6h=lT|#Q_4=wm(O@Tr+2YfbDaPGg_b86Y0lF1)vUD;CJ}${eVO-u6m{;ap8SA_qO3Ih zqKY&D6hiPf~v`6H+2uXi9p>IVe+mkoPk?*e9U@Q9aucPU|Nr$iVmgtzLw10=O*OD7gXF z$N?zlEQd*TBFe;N#tv}q3D#lE^V~zPP1=d5a-Pn;I<;FkvE>*k&e^=l?@jQZ5EPdQ zgn1BDbJu{icwUzs`%iH-NHrne0J|WbB{>9#?L&XXA%&6~5;S}w+4|({e&coTV7H^p zcl=A%P(vqMe&SapW7q+`%zI7yB{a-<>up7@_zvpwg6?>Wo2Lmanhm~meI-G4FgZ;! zIcR2HI{$tFYQ3wSEQVfwGurTrVFceu5gObp9_~0eYk|&jlN-;GttBFMXN>S$VQXX>dQCIkZR4>{8l->4(pzYL$?pt3&mPf12s~_^uPuKZmfvg8 z{>3#zbkp*i)K(%9#iY-#9d=y8BepSLI_>5vhL&8Gl9fujBAL*An1=wb3b87aWrf0| zAI`5~qfpo~PDk+eGx5{LW|2*#Dxs9m=%!Ci!xaH88n;-y^lFTF%&?(XW9rg}HG6+v zjdjt2Ram^e$QdF3bc(J%n4!YcF&l6oN8UJgG7}e-LItP^pu#K0o(z!5bVDBUK|>P^LQ0f@c85`i zx#U~*21%{Z`ZE?i6k8^8HE&0#vitEmSQ_TuOCkcEmBD12n5|tizEd18`%rNUlASgq`y~Q6WQ|mPcIGSM?dbPlZur_0_D|Ds(_|8WVarcks$m}eGraJj;AZFe@#c) z%|pD?g}|tsaJwmfz8Xf0-mZ~dNlB|e*^ZBng<0N+7=4APDvQ|J;zWOoWQev1n*Xu! z)XJis9=w*Pz--OCO57*4a5iea5whQ5Wy9ycZ%g@rN88uynqW?@5x&H}r(-fU%>$YB z?0Kor2NJjYndz-YJRy!lvJd#6ssp0cNzYYh+w8UEad^(kewzf)P=4^l15#s(pm&y} zV-lud)~%xQh@^4&Ws`rFd#Vi@hhJkp<8WwBI@Y+0N|4mt7RMTIx(6T$rZc3p#u`&# z4UIJ>;byFH2Os$u)-gv2R;@ACm~L$tYh2;>#Tttw92aYh8mYw^XDWup8ZY!>S+T~U zX;iH7*1!W>E&UiOD#Yz`hY2h2*DIW#D=9}MepxS zB12^e$OlY;&NTxOSl0CO_}OfJ#qp-<2mXQXOkSjeSau}swmGN(&86LXq(BAp_)&J; ziL`%QVd5H?nMEP8#vLjfGToP^S*2WJ_pOGOI0n>K1L-b}go3|^Js|Ds?V8#rF13mf z)J5(6d$NoZfCp(R1p?C3^J6zk#H_Il&{CM+pPL_fq|%sqW{5Z1=M4l}WzGI!@2T=@ zrl8=XMZDv^r#E|Dx#`ei2p{rv=D?Sv*dBj*|J3&c%v_bU7VtFqA(uSMc*1vhDGyd} zaasx%QbsKujR`e>ja`RWgjzCLQL#=^JNnSIO`NFUG3zl`8#9If9RnWqMiLyVqV#N) z%Bajo$dDdfz8Dl?@lO9ZDNBP;aJg|5>?por+at<}tK&#<6Dh#6ExoXznd$^8z5RcI zY2Y9*b?$S#6+X>lfMmaFVY-I~)4<>7sQn1|(xLmsAe}_*%VzvGI>9h0km&P)^(+m6 z+?FQ`+Xy&Cw3bh4%WkTj8FQpVm-K>-qhJi8Milwtv=bJ^dxx9Ia6m;p(VkzbRRLg8 zGYTr_=vFUyo%j1)1cJMU4;^>fk3+!G^U#RDfNQmv&m8cpe zMVdJ|;VTZNumUmL(Nh%ZzF^j3tWovyKf2tKB;^cJwOi7(ZN}>ir9dVJ7qKll4RUIX#QkHyU%?h3O20R5g$e4Dg@k zmG?@=$jwfOkH}#js7lqh(cORFlj1gcy44#J72tl|XhLKC#g@(e9O4*t1`)6c&1|jm zUa6h{?Dz2~HYs3{bpZ{lFzZk07qw`8Wh>Dr%1=;Q$|o~Y<&qMk(boodecE*ZcDDbx zlaYvnbfe(jMy=+<8uFL8>v|p$Q4`2KiTee}Jk5jr*-_TbOzzOF_{4u>!#+F4+IMzc zvO^c<5|7zSL7yG`%_}xBbkQ4vIfKHne_(*P(RX_%@n_%19qlZ+lEfs>y3O>9f;-~x zdw>dOGC#f@oieze^i~143rvl!Ra_ulJZ}rk;(-dyaW@SKQNn=Hz1!rW!wDA%J1x}q zt_2!1BdD^#D`vrcI2nJCEEf}HNsnxzk^8dyPQ}D7;|Ne3)els_&D{I4&)B{|oamLb z8Y;)Z$95ff0RBvQ5uTk{I|5@BZEXbk&K3*C?Ji&jqhrOZ(QfkWwqli_HmJAcv1J4I zBO7VD*0T7hvWGl(sKvY;T)hzu&tgmqh0c9-Ji6o4X%(V3K`ej6h*-EQ{#ZQhu|_XX zU+ZyixkOSRLNBF)+<-DD2`+Ea2j1#fY8e8R4;Sj~$ZLZ_A&ZmR1{L@-N&IG?0zutCYwoVcmH)s1V0yW5?yzK@lIw*9L{# zfh54zW*kTh5G{Yf_;GkqE}y)e%^wA-M}J!cxJlEwCM>?c)yp9LV=M9RIbKyM%u=v> zYCkuY;F4WiU@04Nv>o@{g@?{N_K!RF$J+N4&+)ZUt*w6$s7mB=hzUFF|#y>oARIPA8R%M$rD zw$Oi;yR~nO-5BNne!o!f|2Xbya7&*DNrpXK{!8Y39uj|2$urws^O<=_m3_b2s9pmL4ILI`3ApKWp1i%P_703F7~$cr z)-=577G-KIh>rvbb*7RHhLTdk+`Kg#;kAE)(ifm*x9e*WuMfO@J00fgjQ>B6${Xwo!}KRmptIeIGjT<>`| z?D;|> zyf!>Qm1=uHXIk#i`DgE{V#ZvA{Z(;X;w%Z2m^^Pjr{>*_&JcKfs?|?Ra80qBc@h<1 zrGEoFNu&cGo(KnC#z+T(y}h$|vGe>4OYE{>Dw^$k?w z)C(A_mqodT!DRPLG<5aKV!ixsSp6|9g@*$Bl4D>~j6R0J{Dc8ks)I=Kc!UZ33S1BC z9g2QAJMj7fu84OeSSFDZV5)>%_bXNYg}p$xNQ<7z9uFW^l^yA6GHx-QB7=VgE3p(^ zY#a<9Fl?e%0pvzezM(Mf&{DF9LkcL1oa)EP;E1)1NelbfKoE)&wtq0@f@+k7el^XT z>PPpz=>Qiafa*rb;DVNgk(1K4yILP+TUYB<$6_H1VYU9)|ECTRIAF zRs$hM)4i9jrlAJhKNx*7`>=ntc}IP9TMM!yht5;}4NcW!{X{ucU%H2Xrj&oreNa(7 z5Ru|1bE0+>Z&;HPtQKT6eLdU<&Cu<$HOhw1Hhu7f!vRW%_-pMRALNzv{rCp) zbF$m%yL{=npTw0Mm45Q`(YjXH6mQ zTR36VXm(6al@MX@ud#nNQ&0_Vd4;mbJh0*weLg^M%DyWlwNdY~$>C-tuj}CD0xtiG z!_Fnc;RO+gI!H@+F$GhjbTT2iVNk^{!f>{A;1{MYfms?@v;HW*CQIK8dQ-28f>VzF zOUY!E-6$vFbJ5tYF0zod38e-CM~&}tAwsXaHEk+G`zp*zCF*~W;H%C0NuVrHAe^Wg z{%&W*aI(pzCCLN)`ylzdCf|sSK3Ibg9rL`IIp!jJ#(=e>^Br=Lw47F4G7PN;z;JJj zGVN-R>J)ODSX;ZqD}sOhr;_V#qX(hy^NhVs=bY!v&5+HXP4fhVA{6i&1)j$6!0@kL z1*isvr&rho1$lp7R9d{Y`;iYK4t&eMx&g)`L+_iC(e#>3WZH&_OR&KUM5MqE6JNSX zT4P5BUI<^Xk&Z1zrJwkdD2Y=UbSt|vy|&9IrTVEZ>bpF4B=}}$Pe)Uj{H-Q9ONS<^ z3M0%FPV7IgY#f@Un*DR#J+rC5xg-S5`Br6}0H*~A2swY&m691j-IFC+Tw2$5-LWi3 zo4iYVpl8SZo5LQ)9LP^X3RAeoyyIt3y) zWm4=-9X)mB^V~{r-Cus@o697$L&k1g9;An6oA(S@6&7CusaF-b`y$eG_L09G`kJjk zs={ljF`fiKHTMJ0lS)h{o!;!r&_Azm5`yRFc_x2@#4h72nHi79PVSRS{2~PC(DvI>$1ARxPATUsi*H`Jw?+`69XnPFy zW}KvZCOn~MBV`i7)NyhhsprX26GNk77k6t$Zo)A2_;FRC9+JhRLHY+g<{VztD#GR5 zDVlxx4A2A=+DXaw&#JN828& z&^xyS%yJG3+ZXwHA%7DLU}r-(lDZCKdjRpg;2?vvt$%C32IjNrRWJHfyvk}miL`&- zu$9_y_Y9a$^*bT!EE1Soe{(F=zY2eFPS;u>s7TT}Ufx`d(IOV!^HGXjU!6^6rC>FI zk>Y+xEe=#ON%Dk=X4>2>kz$ZuXBngly4G?%zT$-H-sp><%#lwCY&Ma(RN1A`OEDP; z8<`JC&{QRC&NRsbO?7%3l+_<|6)k^tOF~ZqrFNz>xeMUP7~@Gz9ncg~h?COrrL!Vg zV%BnXEJ0kH-$q={skA7X|F;|E9L~T4iuoVrOj|Q>I@qBFIa8kRPHB)LQ2-ss@H9_? zw*W_960atEvpaW#zS66Ndm9|cndH2yvB`w4X|r#Npk{jIL=amz%2H%|;UKYE>D2?a5l~NdA7Uxj^J+xY zAsL2Es7wQPvq9KQBu$O@;#&gTH_Z-xWErRE|J&GO4JN$aJ!_#G zKaX14P#V%+McJ43Y>JyzT(ob&hkMXq*MFdw&*NM!SC>6W9Tue|NyLq@zooWX2DpBK z1B#Ovoy`qVKNYLN&|`lN76KIcYlK(UVkC4aL{d;%phY4^I=$%q`fMOlyji^j3LfS+ z+;=|!nH{bt=Xu+Z{E9LAwW`_M1CM$gvot0++ubxQ8SA>Rc}(qAuAB2Lo)SW^=#yQg z7g&;&1@v5g02v-<1o?z(nic(7#8$#!$w<2E8ncBP!Vau=eVc#b9^Ozzo<`D^Lq!W| z^{*3uOr+kzFO%E$h7vIxsdwwAC{@*gM{y8mw)rN3r!8kE7qp4A!>q|Br3}M>70K;T zybh9z{2Nz>-Uy00um>%T^{L=Mn->95}jc~bSS#t{*_-|ho zyyi5eC_1u0e%>{U-@il@FWI)V%U&+l#?5(D5ssTq|McHNzs#32?LbPQ6XhA%K^=Iu0bQVxB$70*$``N7^)-#r z|E~-3aSs1L+MJn1(*OVePy%59WB_0Q${DfpN>zW{m@R2qOHCv1#W`6DuR~Mb%!!xt z)JM{#waBsJOIA56t&cA$>v>&ir_{7BY+N4v_LH_*fZdiyJ=G(1QSQxCg%YoMd;S_J z-bvF7LO@W!2sW>wX7l!1Y%vRR?&YSl=J7co;DMZ)P)`2pG`n>XfHP*)o}FaSuebO6 z<@$fD(-PE!tg7=?#9e9TvED^g6d)cZ9CL*g6$_@@d&=eD6bIbI#d2}+s3hmsGw7{G zlOF$>EZ$~MK39(Rx8*-EuEt^!XO)RWKJE_0W?sfX!E9URT`uBcF4m5IlK<`oC-2Wp zzF!YhUlWX2Tco;Srsl0-^9EZs*fJ8nDT;qmqn@rSc>?_JYLZ`2wV_!uSSMuhkk|2jU1B8QvXqf>_fk(Ou+Xi_!w^jru{zCqJ zJ;P2tSSVC;G7RS|7{))8(GC)Hy2<6q~{O`*9c4J@r{eI}+@aLc4!& z+k}cEAOeQ{sB;~eO_HPoLm&`Hpg_SPc{mo3gaUFQA%)`)BXHo5&{!-A7C~Wf5DWwb zfk40!1VIo4K~Mxi5QIh;lmQ~J-ohHz%w6ec1W0;V&smUv!(-r~pRxSA`Dy8|C;Rgj zdh|9sB4wK#S&i1w8Skp83%Cd~igACfH;XVrpU2Zf5%-osXZzwf@MZTUiw4*^ivHFx zD?~RoaB9n*)gGvQ8`@CCRp+Rwr&KvnN5x4tml#r!ru?pHFwTfL8GO-4_?sZ%)$NuZ zEKG24?Z@!KRPpyn--5Ro1qY2^2_Q1_ zv6ZqY{kPoL=6d9pBU?#qhgs2vlRbJM1V68&UW&~n5kCx<5q?*LeGY= zLbY+Zy407jD;-;Y89)~0Cs4Ji0APVJgfsHPdGd}lCsTZSK~87&UHz+Xh?Pchf?Io* zXKh*-qC^Flndgk7baU!vU^RcFT^R!P+G>xR=y5vg*4I0$E6FIYcu*5uMJZ~7el2eN zo$v6;VMTbuC4;!+kAU81*RCOCVkba_xH-Yafx34KXKG{EED&79 zEcx9DHCTgo?X3PpF@4qryzH;wBRx#Y;?EI9HH@YN$5Q1E3p8y?}q%~sX1!QMjUrcP*w zcY8|tNvU2h)WzVA;j$hnsJX@gk4sd5q2%NH?j@Jrm5EbIqLLhCgU*u@wTW8QZkgV> zI0kBxNA#i;oRo)ULK}a=l*eR7%OM%{O6=-rvy*Dr_^6Y(FXN9Ngbgu0sQ@Y*FVj%k zb*Y_9@@XG8#Q9vR_}43>d;`Uoffa>aPTa+LUm`97==y-<9z`+Yro{Eh7|#2r_3|3o zOtL4e2D(5)lhdvSnwzEJc7s?0JyMUig}LtPvJXvc^bE8T+3?~-V)46Z3LbYQ?h|2J+4APDAB;n7H=A&noGamJQ7eiKY@#4_l1QsRjr1fD5ur83}3l=?7P5!~YU487Z&uMY)MQcae z^SxM8jcJ}GXH$QBcaMOf?(OB%69%0x!KY;H$P40UrzU9!G{NXmIDyG6 zaWdC7txP7&x7_hWgeDO&`nl7yqv{ygZ6)TFH3iSzV%zuLt`ka4w{`! zXlJ6ZQ=OFT=wS6qNat%Yg6)vf&h~E*Iz}>!+-{THIzcQn6@az<6&t;o z0PIKl;viR}Y(y6b$!lH2S^IhEEE0_Z@Ha)-f_=&7ELOrXt{SHNMVi0T#3O~2rY#6l z9sCofRegWXsB##P4k+m^lr=!uCuvnBYa-9!Z_wNrOfZKCvN=5d-l;ayAC5DfzRO8r z)3t7KN$ zk!rT(QuIKq?{o9+GK!qRq(yNSl_aKm&13mH*Qnuj^lBr*q%hcbA_CoN- zLa6w?QIa$f@t;M*Prku+IS&P0ZutZLy%7391O`t-d9KrR@HM}n+@CIwiZZvc#8RrM z5p#d~ojf0F5z}Ai2pX_+=b3|S(<~S`Wrg_7B;DD7nqOK((D)Ep88SD|_=3i5Rl`i@ zX3j_GPv?v=LtQRsl5z>Of_ou16S-3n+A&P;)WS%2Sa|v_;;Ol8m05nFaL51*g8N~D z^>x5lABz-ZrKA%Ev<4@QUm}(_v*=k#RI`7)xnCeM^Sq~6S}@q~W{OA+Z!Y}wXf7IH zt)!E1)ciq4T+`7rlc<~5Qv(!L5X#XJR&+xHPoRq!-h`!OC9^aRAE6Xp} z*OsmS;{&*fW#fk$loTPBT~Uk5Uji$KM@jmP0&n%fl#Ma3oM~-;HV?EK9j36|=zo7f zf|})Ak3w^1#FL(CZF4#Ig~#&Hra(@o<(#wUj)hvzG!i(HR}R`)X5Nx#=b^Cmr<~MY z4O_P{U^EUQRXl@tE5E~**h8yRJ&hNo+(xRTj0~%u1U5Vu)45DksauRQw=9EI)x@p@ z*g?}I->X6}!BqE|tvh6kW4FHWhcSO&-Q}UoIz{7-iXIx)?Q8S!54Q90AI~Yf&N8#!3-{G8YsR=9O*17RKz9Xk8%Z=qN6yA`R0wVaYo>@KtI z)2(9fQU|PYEvh$nu-3+v(09hKx z(0UAkKe`KOrl2^(OX0_73-7b4;h`iDg%6x*U-^P^0_*2y4&7^KBn;>n(HUs+sDqzz zt2%jze8EI`4EdxXv^$iX^?ZNK%&+UJzIxSI%(1&%h`ov)`iJe!@PLY@_Iy)44(uvI zCgHKTe~YXB;1KnI;{Cv2mRvei08SE_VX{VQ$!~#~hc^|BaYjjF!))J2Qwt4)MoSe@ ztc3j;AyB0h3$bi5?sb23f;L8G0PM@jUz4uwKi2pniX0%erb_B&VPk z8tJrBIyUfeW&+UG%XJH8v^(*dP-N(7xl5wrPtb!4>Xn6jZXTO|ln`2ij&M9dU#27)4}_GA=0Mx;U@a%ryXUtjQl)zp)TJDb180NFVqkyd^&P$)_)N~IC1Z2dd$V-rb zOIV3)gtT$8{%DCT5m`LG>Jw|8niJvf_m3KV^zyO=UCyV6i!Ki3k@3-sugyZFbbZpD z6c2Em4pLF-x)C+2%CKc|63h_&4-HM9vHH+Gp$C839MRMkPk4U}AL56l)x*e&uDauU z%xJQ5TtF*XqH`g#GjY>glko?6cN3&{f#9w-t+Dr^&2YdF(lNPBEYcnwGt8NtqcC`A znk(?fLvFHy4o$%!!kZ|XkPInk7urvQFe!y@N>YkS(&F0lR$M2oA8(MPI85*tZ-Vu; z01ioE9Zd%Rg`R)B4p+KDVN6&J>?gf2s2wK~4^;ATNc&`(3|JnNk}%ILJs+l$Qlbhsh@7EUR3m-Dj-BNAeXY`1kE%5RdZ@Q#3lX^H zB21nE`I7-Fa#xA`oA^{r=FJaOzIjlkkp!fnMiQ{DNsxb)p|x(A7$ke-N8%OnF064J zu8_zrkV{&IaMa@tI{~X7jU>>2;4P!L)Vk2a|^megRJQ@ zgY3wWvkre!07ObrJ*ipTA=B-0o>xe-xS@+pojDKmNQtOE4>zo?yfNfhPL#n-Gx6AM zX2#1L!Fu(Jo*uBboV#gM^nfPFzz~^KmJ$sn3QKiL&C*q10ci}D^K%XJEmj>oV9l}R zRVq_w^!Qg;HIL-89<3hM7ngkso07?#Jzd&hbxvzxf4$^?19!Qgzas9WlAB$>1NcBV z(*RQc2eZK7a$R1QMyQgU^?nTMhIbC^;*o}C1aG_h`j9HpY2TM0jrlP_pnH-mqbxyz z(`l8=b}!n*tx@BcSvi$WG5ksH^FKI8Gh(2=MkGcTK1JU3rmHq7OyZ1J2;G}n8^zo& z^Jt4q=Ev3 zlHtOCq?88Ygqv=m4<3ZWn;m@5>n;g$9#Zy@{yDr@Ohe#34p&1kpUQM($m@g!eO&-b zDCmZDU@0T(LrBd5Ji)sK39|o4;6si>*Jdo|Sh!LcBmcia5<+&HAYex96^l(f+GJx7 z1)x+5P7Xhst45zOJ9qdc3G&bwuO5WHNujWRc?Ak8NigR%tnuaWtCltm=pTeB-zljN zqr7f_pR)}q3|k2eeR?z*S`oaadOl9D{M4HHZx6mJ#F1w1namV>yC^*8mSHDzjl%aw zPX-7d5UmZEo{_Qs%yv~Tccim|;Y$|tXX zdeDYNGs~05k|U0XwtcCZbU3erD!mG$1)=JbdZk<*;mg}k&YB6jY}voM4L+>{;gxctsI8(%IE4cz)^wH?K75dj%8AId=m|@F&_ZNW9dy+BwlNVqssU~dM@{vyKwQ}qE9Nd z8UTagkQQwckAUD2>v$PLjf!&bmRAPgbd+19fJK$=kL#nOb}ecYWf{rVidV$%!6vX~ z35z@IWgLx*o@0`CJ`Dec>_TjRa03RNeRH#!IxrIOf8Y>8V0e2Ev0!OxD!K@dK3k&Y z5CerY7d*Q1{kIbd5_ACUOU!C9s!0)VF45=a=QFr(p+|ua5DR7VW>F1YM7*jFAxfMu z;vTmBnO#U95keuzDT55f(LBzMzUb4roePlqc8w8F%RdU~Psq_|$$0&LC4L-o4Dmp= zF~q}d8PrDy3#3Xc@FZ!tsE)OW$I~yNBQo97j=t90EO?axPdv1csOkstttxjYb#AVY zn9H~Q|2r|Iq&3vg3AarKYbuQtRvj}u0_I{s+uJu_fooZ>-w^^K-fl-M0EK4Gq@g0j zuZDR{2|S>yh%#@#_C;8K&OWB5*0U3dK9an1`i&IQIxN^G-U=dzPkRGj41i;H4AQ*s zXcr7-+%hc4`BN>*R054YwbzzjQ7DF-k6|s(Cj<`eTuL7(bYGZ|UVwLYU}nSTpVEJw zz(c;Ic9Z0&4Bpt0wNaVn4L8b>`CF^pbHT(JgS*`mvrngB%b{9-7|NMJBHq74fKbd7 zq|U$+Ydw}F#Q*gpwQa81fF*du1@{iY#?;4w!Z<@OTV6LguB?1bx0tI?@@w8-bhYmm z;X$vrJ_a$QPH}BvW)4U3%MRGE2KB;_rz`#rY(Y3oPE5HLElima|4R!1j>4&|5ElHp z5T(Wxeve$PnDE|z43QoS&Jy|&i|JxxtU`Ftztn$jM*Q{(lMn+Zoy45Bn1HO!bCmKZ z0MjFrNKwtBawuSSKE;{|=$(=2+<}eTw2k9jUkZA+$8NR$S=9$F?s4G1nBKGF=hvUvb#W8w|yWim7)`Z~(7B2Zn};}4u=)qkz=xkO)W#rbO{ECMSAb1OKl=(-spHrT zI5&8G)t0>|p~>o9@@8|37LK|W@2t>D2hchB+ZxUqUCwc9 zpW=zRb){{8Kn%veE#%4kf|=b*2bs>Ws&SpsPej)(-z<%dG-l{zlG!?1k>Y~09sVwo zNz7!*`L+@3CPQ@mJ$(qF)7(gYH0wsfC7NE8yWj0Ym{rkVm>u=Qg_F4wb<}Pm$F^r{ueTNQjQy~F8!SsCh%T?9^eyFq= z4&bT4xA2vb9au(A0AsDJ$&>}-yE}lbk6hrxra`UTXf#mh|JVB z!DS(tCuxE&=eG7MYnc363TsmxjBMDW8bM-lCj<<`^&Mg-1RvJz1MlnTJ}{E}jU)Gg zgC@Lx4?Kmz?*r#Dgdh0AIA5?Q_<_HB$PY|LTYlg<73T;36E{C_Bq{U*Kflut{K;(n z!11ub_w$L`ys|f-*41~ZKbi`-@>7=ruW>)H-UniI8%L%doyOsjnSUYIFLT;RWkdA3 zZ}Vv&@J3Mk4i1_}vTx)6bD+#e=eoCNQKM*o@o>nPmxz(c7saK|GYRRTI&VaTP4|$M z4Whv3b2UOpo>Dk`IJ}pyVlt>vhByY1^KRHGJ637fi2-F609+2ybQ-6D8M5f$Ao(kH zd(0Y0(dG-G(yuf3&+i5AM%2s?MHdQ2m|Dw~*AHQ}riK?#v_nuYpuV$4RPLE9ma+SP z7^cTLwNs#AS*X+85GjP^L6gBJhKPsgXiAnmI7c-|5&f6^>7Mi23L4p8Ck(R3A_*C| zl$NlW>8#yiVijx<=0dg<#%9~|?g6i{5VTN#B?UL1Dc%NsmvQL9YNyC;JKp1fH{=_8 zSekq>GJ3 zTCS-og@qMEHv99yT7_~9nr7hp>NjzmkZvepk|`Uz%7e!f+8}s&F>Qd8D9A4Cit`*H zbjA7nqmXx^69B;#=jk6Y?XFtDfv?7w&|g)cHds{*=5VCQW<52heqH{lF$MONO_Ok> zO;^no7p%U+SMvYPzV1XKvlyy>0Nbo8rrOE7M zk6vXr42x=T0Pj53JT)dKHAJlCcxucd1yu>JJ2jST)_9n`yfN8Xo-1r8r@BgsnH>uN zcxG&2Wn^t-X>N35ZF6URc)Z~0$X4aY2+6GiUIoLY%ev(H6W^Er9%%=Ez%$8iJg41P z8t@QK8I#B+B4<6;_q@-H24k{Rn?C3|-n>ztd_zCBjzX!AuawK+ybo6su(<>R8**#N zy|-W{GIe|n`9^#0cA!CrTvHU1W(X%j!17@$Q;UJ!K&B<~9{CZnf0R^$WP&&?eqKsp zjrK}h#s+;9OsMhQLTLwo^13C=*tz`N>9gyOHZ)QS_B);T*dEqf9sSb!{?o8nr6v6( z20N!9WWkIONZ2|FjTrmKQ&kE?p9BzMs}D6CU|^QV1aJ~i{)|QbP^OBSe=$t+4x;FJ z=d, pub status: TurnStatus, pub error: Option, pub started_at: Option, @@ -131,6 +132,7 @@ impl ThreadHistoryTurnChange { fn from_pending_turn(turn: &PendingTurn) -> Self { Self { turn_id: turn.id.clone(), + root_turn_id: turn.root_turn_id.clone(), status: turn.status.clone(), error: turn.error.clone(), started_at: turn.started_at, @@ -1242,11 +1244,12 @@ impl ThreadHistoryBuilder { fn handle_turn_started(&mut self, payload: &TurnStartedEvent) { self.finish_current_turn(); - let turn = self + let mut turn = self .new_turn(Some(payload.turn_id.clone())) .with_status(TurnStatus::InProgress) .with_started_at(payload.started_at) .opened_explicitly(); + turn.root_turn_id = payload.root_turn_id.clone(); self.record_changed_pending_turn(&turn); self.current_turn = Some(turn); } @@ -1362,6 +1365,7 @@ impl ThreadHistoryBuilder { }); PendingTurn { id, + root_turn_id: None, items: Vec::new(), item_index: TurnItemIndex::default(), error: None, @@ -1593,6 +1597,7 @@ impl TurnItemIndex { struct PendingTurn { id: String, + root_turn_id: Option, items: Vec, item_index: TurnItemIndex, error: Option, @@ -2033,6 +2038,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2098,6 +2104,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2166,6 +2173,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2259,6 +2267,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2376,6 +2385,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2476,6 +2486,7 @@ mod tests { let items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-image".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2869,6 +2880,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2936,6 +2948,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3071,6 +3084,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3150,6 +3164,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3234,6 +3249,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3330,6 +3346,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3424,6 +3441,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3495,6 +3513,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3519,6 +3538,7 @@ mod tests { }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3601,6 +3621,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3625,6 +3646,7 @@ mod tests { }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3701,6 +3723,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3769,6 +3792,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3837,6 +3861,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3861,6 +3886,7 @@ mod tests { }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3917,6 +3943,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: Some(10), model_context_window: None, @@ -3932,6 +3959,7 @@ mod tests { }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), + root_turn_id: None, trace_id: None, started_at: Some(30), model_context_window: None, @@ -4019,6 +4047,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4043,6 +4072,7 @@ mod tests { }), EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-b".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4089,6 +4119,7 @@ mod tests { let items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-compact".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4361,6 +4392,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4423,6 +4455,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4482,6 +4515,7 @@ mod tests { let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: Some(10), model_context_window: None, @@ -4552,6 +4586,7 @@ mod tests { let items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4612,6 +4647,7 @@ mod tests { let mut builder = ThreadHistoryBuilder::new(); builder.handle_event(&EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4640,6 +4676,7 @@ mod tests { let items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4687,6 +4724,7 @@ mod tests { let items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4752,6 +4790,7 @@ mod tests { }], changed_turns: vec![ThreadHistoryTurnChange { turn_id: "rollout-0".into(), + root_turn_id: None, status: TurnStatus::Completed, error: None, started_at: None, @@ -4846,6 +4885,7 @@ mod tests { let start_changes = builder.handle_rollout_item_with_changes(&RolloutItem::EventMsg( EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: Some("root-turn".into()), trace_id: None, started_at: Some(10), model_context_window: None, @@ -4858,6 +4898,7 @@ mod tests { changed_items: Vec::new(), changed_turns: vec![ThreadHistoryTurnChange { turn_id: "turn-a".into(), + root_turn_id: Some("root-turn".into()), status: TurnStatus::InProgress, error: None, started_at: Some(10), @@ -4896,6 +4937,7 @@ mod tests { changed_items: Vec::new(), changed_turns: vec![ThreadHistoryTurnChange { turn_id: "turn-a".into(), + root_turn_id: Some("root-turn".into()), status: TurnStatus::Completed, error: None, started_at: Some(10), @@ -4943,6 +4985,7 @@ mod tests { }], changed_turns: vec![ThreadHistoryTurnChange { turn_id: "rollout-0".into(), + root_turn_id: None, status: TurnStatus::Completed, error: None, started_at: None, @@ -4960,6 +5003,7 @@ mod tests { let changes = builder.handle_rollout_items_with_changes(&[ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: Some("root-turn".into()), trace_id: None, started_at: Some(10), model_context_window: None, @@ -4982,6 +5026,7 @@ mod tests { changed_items: Vec::new(), changed_turns: vec![ThreadHistoryTurnChange { turn_id: "turn-a".into(), + root_turn_id: Some("root-turn".into()), status: TurnStatus::Completed, error: None, started_at: Some(10), @@ -4999,6 +5044,7 @@ mod tests { let changes = builder.handle_rollout_items_with_changes(&[ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-a".into(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs b/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs index f5a1e238f5..a0b2a14ac7 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs @@ -23,6 +23,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet { RolloutItem::EventMsg(EventMsg::TurnStarted(event)) => ThreadHistoryChangeSet { changed_turns: vec![ThreadHistoryTurnChange { turn_id: event.turn_id.clone(), + root_turn_id: event.root_turn_id.clone(), status: TurnStatus::InProgress, error: None, started_at: event.started_at, @@ -34,6 +35,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet { RolloutItem::EventMsg(EventMsg::TurnComplete(event)) => ThreadHistoryChangeSet { changed_turns: vec![ThreadHistoryTurnChange { turn_id: event.turn_id.clone(), + root_turn_id: None, status: if event.error.is_some() { TurnStatus::Failed } else { @@ -58,6 +60,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet { ThreadHistoryChangeSet { changed_turns: vec![ThreadHistoryTurnChange { turn_id: turn_id.clone(), + root_turn_id: None, status: TurnStatus::Interrupted, error: None, started_at: event.started_at, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs b/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs index f4abca35cf..4210011287 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs @@ -27,6 +27,7 @@ fn projects_turn_lifecycle_without_prior_builder_state() { let started = project(RolloutItem::EventMsg(EventMsg::TurnStarted( TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: Some("root-turn".into()), trace_id: None, started_at: Some(10), model_context_window: None, @@ -49,11 +50,16 @@ fn projects_turn_lifecycle_without_prior_builder_state() { assert_eq!(started.changed_turns[0].turn_id, "turn-1"); assert_eq!(started.changed_turns[0].status, TurnStatus::InProgress); assert_eq!(started.changed_turns[0].started_at, Some(10)); + assert_eq!( + started.changed_turns[0].root_turn_id.as_deref(), + Some("root-turn") + ); assert_eq!( completed, ThreadHistoryChangeSet { changed_turns: vec![ThreadHistoryTurnChange { turn_id: "turn-1".to_string(), + root_turn_id: None, status: TurnStatus::Completed, error: None, started_at: Some(10), @@ -90,6 +96,7 @@ fn projects_failed_turn_completion_as_snapshot() { ThreadHistoryChangeSet { changed_turns: vec![ThreadHistoryTurnChange { turn_id: "turn-1".to_string(), + root_turn_id: None, status: TurnStatus::Failed, error: Some(TurnError { misalignment: None, @@ -240,6 +247,7 @@ fn projects_identified_turn_aborts() { ThreadHistoryChangeSet { changed_turns: vec![ThreadHistoryTurnChange { turn_id: "turn-1".to_string(), + root_turn_id: None, status: TurnStatus::Interrupted, error: None, started_at: Some(10), diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index be146d82c7..b5171d2320 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -3281,6 +3281,7 @@ mod tests { "turn-1", &EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: Some(42), model_context_window: None, @@ -3316,6 +3317,7 @@ mod tests { id: "turn-1".to_string(), msg: EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: Some(42), model_context_window: None, @@ -3592,6 +3594,7 @@ mod tests { &event_turn_id, &EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent { turn_id: event_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: Some(42), model_context_window: None, diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 875306fdb4..818ed4ba95 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -1194,6 +1194,7 @@ mod thread_processor_behavior_tests { "turn-1", &EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/app-server/src/turn_cost_worker_tests.rs b/codex-rs/app-server/src/turn_cost_worker_tests.rs index c1b62699f7..efd940492c 100644 --- a/codex-rs/app-server/src/turn_cost_worker_tests.rs +++ b/codex-rs/app-server/src/turn_cost_worker_tests.rs @@ -88,6 +88,7 @@ async fn handle_observes_only_matching_model_provider() { id: "turn-1".to_string(), msg: EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index d19eedfdcc..0253646ae5 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -1102,6 +1102,7 @@ async fn thread_fork_can_cut_before_unfinished_stored_turn() -> Result<()> { &source_path, &RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: unfinished_turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -1376,6 +1377,7 @@ async fn thread_fork_creates_reference_backed_paginated_thread() -> Result<()> { for item in [ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: Some(10), model_context_window: None, @@ -1705,6 +1707,7 @@ async fn assert_thread_fork_freezes_active_paginated_turn_as_interrupted( source_path.as_path(), &RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "active-turn".to_string(), + root_turn_id: None, trace_id: None, started_at: Some(10), model_context_window: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index 48633ce600..85c7a1e86a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -2222,6 +2222,7 @@ async fn read_items_page( fn paginated_turn_started(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: Some(10), model_context_window: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index e977da7003..69da60d821 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -3866,6 +3866,7 @@ async fn cold_paginated_resume_restores_usage_without_loading_turns() -> Result< &path, &RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: canonical_turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -3977,6 +3978,7 @@ async fn cold_paginated_resume_omits_usage_when_its_turn_is_ambiguous() -> Resul &path, &RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: interrupted_turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4119,6 +4121,7 @@ async fn thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn() "type": "event_msg", "payload": serde_json::to_value(EventMsg::TurnStarted(TurnStartedEvent { turn_id: stale_turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4207,6 +4210,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re "type": "event_msg", "payload": serde_json::to_value(EventMsg::TurnStarted(TurnStartedEvent { turn_id: interrupted_turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -4520,6 +4524,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is "type": "event_msg", "payload": serde_json::to_value(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_timeline.rs b/codex-rs/app-server/tests/suite/v2/thread_timeline.rs index 6cc196667b..43aea1dd62 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_timeline.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_timeline.rs @@ -97,6 +97,7 @@ async fn timeline_pages_mix_items_and_resolve_the_opening_realtime_session() -> }), RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: Some(10), model_context_window: None, diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index d2a2010b5f..f8cb0e5457 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -476,6 +476,7 @@ async fn get_status_returns_not_found_without_manager() { async fn on_event_updates_status_from_task_started() { let status = agent_status_from_event(&EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 55cc1718ba..28bd8bf052 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -47,7 +47,6 @@ use codex_protocol::models::InternalChatMessageMetadataPassthrough; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::TurnStartedEvent; use codex_protocol::protocol::WarningEvent; use codex_protocol::user_input::UserInput; use codex_rollout_trace::InferenceTraceContext; @@ -149,14 +148,7 @@ pub(crate) async fn run_compact_task( turn_context: Arc, input: Vec, ) -> CodexResult<()> { - let start_event = EventMsg::TurnStarted(TurnStartedEvent { - turn_id: turn_context.sub_id.clone(), - trace_id: turn_context.trace_id.clone(), - started_at: turn_context.turn_timing_state.started_at_unix_secs().await, - model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.mode(), - }); - sess.send_event(&turn_context, start_event).await; + sess.emit_turn_started(&turn_context).await; run_compact_task_inner( sess.clone(), turn_context, diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 376ad06c99..7a5d3ad416 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -49,7 +49,6 @@ use codex_protocol::models::ResponseItem; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TruncationPolicy; -use codex_protocol::protocol::TurnStartedEvent; use codex_rollout_trace::CompactionCheckpointTracePayload; use codex_rollout_trace::InferenceTraceContext; use codex_utils_output_truncation::approx_token_count; @@ -111,14 +110,7 @@ pub(crate) async fn run_remote_compact_task( let step_context = sess .capture_step_context(Arc::clone(&turn_context), &CancellationToken::new()) .await?; - let start_event = EventMsg::TurnStarted(TurnStartedEvent { - turn_id: turn_context.sub_id.clone(), - trace_id: turn_context.trace_id.clone(), - started_at: turn_context.turn_timing_state.started_at_unix_secs().await, - model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.mode(), - }); - sess.send_event(&turn_context, start_event).await; + sess.emit_turn_started(&turn_context).await; let compaction_metadata = CompactionTurnMetadata::new( CompactionTrigger::Manual, diff --git a/codex-rs/core/src/compact_token_budget.rs b/codex-rs/core/src/compact_token_budget.rs index 60d25c53d8..3e6ce1beaa 100644 --- a/codex-rs/core/src/compact_token_budget.rs +++ b/codex-rs/core/src/compact_token_budget.rs @@ -14,8 +14,6 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::items::ContextCompactionItem; use codex_protocol::items::TurnItem; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::TurnStartedEvent; use tokio_util::sync::CancellationToken; /// Runs token-budget manual compaction as a normal compaction lifecycle. @@ -27,14 +25,7 @@ pub(crate) async fn run_manual_compact_task( sess: Arc, turn_context: Arc, ) -> CodexResult<()> { - let start_event = EventMsg::TurnStarted(TurnStartedEvent { - turn_id: turn_context.sub_id.clone(), - trace_id: turn_context.trace_id.clone(), - started_at: turn_context.turn_timing_state.started_at_unix_secs().await, - model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.mode(), - }); - sess.send_event(&turn_context, start_event).await; + sess.emit_turn_started(&turn_context).await; // Manual compaction runs outside run_turn, so it captures its own current step. let step_context = sess diff --git a/codex-rs/core/src/realtime_history_tests.rs b/codex-rs/core/src/realtime_history_tests.rs index 48db0239b3..55e0019b21 100644 --- a/codex-rs/core/src/realtime_history_tests.rs +++ b/codex-rs/core/src/realtime_history_tests.rs @@ -29,6 +29,7 @@ fn started_state() -> RealtimeHistoryState { state.observe(&EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -128,6 +129,7 @@ fn interrupted_turn_is_not_associated_with_a_new_voice_session( state.observe(&EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 98bfa792b2..cb51fdfb19 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -144,6 +144,7 @@ use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_protocol::protocol::TurnStartedEvent; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::protocol::WorldStateItem; use codex_protocol::request_permissions::PermissionGrantScope; @@ -2184,6 +2185,24 @@ impl Session { .get_or_init(GuardianReviewSessionManager::default) } + pub(crate) async fn emit_turn_started(&self, turn_context: &TurnContext) { + let event = TurnStartedEvent { + turn_id: turn_context.sub_id.clone(), + root_turn_id: Some( + turn_context + .turn_metadata_state + .root_turn_id() + .unwrap_or_else(|| turn_context.sub_id.clone()), + ), + trace_id: turn_context.trace_id.clone(), + started_at: turn_context.turn_timing_state.started_at_unix_secs().await, + model_context_window: turn_context.model_context_window(), + collaboration_mode_kind: turn_context.mode(), + }; + self.send_event(turn_context, EventMsg::TurnStarted(event)) + .await; + } + /// Persist the event to rollout and send it to clients. pub(crate) async fn send_event(&self, turn_context: &TurnContext, msg: EventMsg) { let legacy_source = msg.clone(); diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index cd75eaac8e..f85604e77b 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -94,6 +94,7 @@ fn completed_user_turn_rollout( RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -351,6 +352,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -423,6 +425,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -459,6 +462,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: rolled_back_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -543,6 +547,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -576,6 +581,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: incomplete_turn_id, + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -642,6 +648,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -675,6 +682,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: second_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -707,6 +715,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: standalone_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -779,6 +788,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -812,6 +822,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: assistant_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -876,6 +887,7 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: only_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -933,6 +945,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() { RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: user_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -965,6 +978,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() { RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: standalone_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -1013,6 +1027,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -1044,6 +1059,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: incomplete_turn_id, + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -1555,6 +1571,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: current_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -1632,6 +1649,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -1768,6 +1786,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -1799,6 +1818,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: aborted_turn_id, + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -1902,6 +1922,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -1933,6 +1954,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: current_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -2038,6 +2060,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -2069,6 +2092,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: incomplete_turn_id, + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -2132,6 +2156,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: current_turn_id, + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -2218,6 +2243,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -2249,6 +2275,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: compacted_incomplete_turn_id, + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -2283,6 +2310,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: replacing_turn_id, + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index aa828752a3..169b055be5 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -618,6 +618,7 @@ async fn regular_turn_emits_turn_started_with_trace_id_without_waiting_for_start }; assert_eq!(turn_started.turn_id, tc.sub_id); assert_eq!(turn_started.trace_id, tc.trace_id); + assert_eq!(turn_started.root_turn_id, Some(tc.sub_id.clone())); sess.abort_all_tasks(TurnAbortReason::Interrupted).await; } @@ -3839,6 +3840,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -4046,6 +4048,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -4077,6 +4080,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: rolled_back_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -4170,6 +4174,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -4199,6 +4204,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: compact_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -4236,6 +4242,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: rolled_back_turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -4319,6 +4326,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -4348,6 +4356,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: "turn-2".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), @@ -4377,6 +4386,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() { RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: "turn-3".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), diff --git a/codex-rs/core/src/tasks/regular.rs b/codex-rs/core/src/tasks/regular.rs index ba4b44a8b5..1dc274437f 100644 --- a/codex-rs/core/src/tasks/regular.rs +++ b/codex-rs/core/src/tasks/regular.rs @@ -10,8 +10,6 @@ use crate::session::turn::run_turn; use crate::session::turn_context::TurnContext; use crate::session_startup_prewarm::SessionStartupPrewarmResolution; use crate::state::TaskKind; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::TurnStartedEvent; use codex_thread_store::PersistContext; use tracing::Instrument; use tracing::trace_span; @@ -48,14 +46,7 @@ impl SessionTask for RegularTask { // Regular turns emit `TurnStarted` inline so first-turn lifecycle does // not wait on startup prewarm resolution. let prewarmed_client_session = async { - let event = EventMsg::TurnStarted(TurnStartedEvent { - turn_id: ctx.sub_id.clone(), - trace_id: ctx.trace_id.clone(), - started_at: ctx.turn_timing_state.started_at_unix_secs().await, - model_context_window: ctx.model_context_window(), - collaboration_mode_kind: ctx.mode(), - }); - sess.send_event(ctx.as_ref(), event).await; + sess.emit_turn_started(&ctx).await; sess.set_server_reasoning_included(/*included*/ false).await; sess.consume_startup_prewarm_for_regular_turn(&cancellation_token) .await diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 97f43df05a..91ed68a51b 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -36,7 +36,6 @@ use codex_protocol::items::TurnItem; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandSource; -use codex_protocol::protocol::TurnStartedEvent; use codex_sandboxing::SandboxType; use codex_shell_command::parse_command::parse_command; use codex_thread_store::PersistContext; @@ -123,14 +122,7 @@ pub(crate) async fn execute_user_shell_command( // standalone lifecycle tasks (for example /shell, and review once it emits TurnStarted). // `/compact` is an intentional exception because compaction requests should not include // freshly reinjected context before the summary/replacement history is applied. - let event = EventMsg::TurnStarted(TurnStartedEvent { - turn_id: turn_context.sub_id.clone(), - trace_id: turn_context.trace_id.clone(), - started_at: turn_context.turn_timing_state.started_at_unix_secs().await, - model_context_window: turn_context.model_context_window(), - collaboration_mode_kind: turn_context.mode(), - }); - session.send_event(turn_context.as_ref(), event).await; + session.emit_turn_started(&turn_context).await; } let Some((turn_environment, environment_shell)) = turn_context diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 7103c17145..c25a55575c 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -562,6 +562,7 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() { RolloutItem::ResponseItem(assistant_msg("a1").into()), RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-2".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2705,6 +2706,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() { InitialHistory::Forked(vec![ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-explicit".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/core/src/thread_rollout_truncation_tests.rs b/codex-rs/core/src/thread_rollout_truncation_tests.rs index 88d496eaa1..71a80665e2 100644 --- a/codex-rs/core/src/thread_rollout_truncation_tests.rs +++ b/codex-rs/core/src/thread_rollout_truncation_tests.rs @@ -79,6 +79,7 @@ fn inter_agent_communication(text: &str, trigger_turn: bool) -> RolloutItem { fn turn_started(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/core/tests/suite/agent_execution.rs b/codex-rs/core/tests/suite/agent_execution.rs index ace2f50a74..b31d821a49 100644 --- a/codex-rs/core/tests/suite/agent_execution.rs +++ b/codex-rs/core/tests/suite/agent_execution.rs @@ -205,6 +205,62 @@ async fn v2_nested_spawn_checks_shared_active_execution_capacity() -> Result<()> Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn child_turn_start_preserves_root_attribution() -> Result<()> { + let server = start_mock_server().await; + mount_root_collaboration_call( + &server, + FIRST_PROMPT, + "first-call", + "spawn_agent", + json!({ + "message": FIRST_TASK, "task_name": "first", "fork_turns": "none", + }), + ) + .await; + let worker = mount_completed_worker(&server, FIRST_TASK, "first-call").await; + let test = test_codex() + .with_model("gpt-5.6-sol") + .with_config(|config| { + config.features.enable(Feature::Collab).unwrap(); + config.features.enable(Feature::MultiAgentV2).unwrap(); + }) + .build_with_auto_env(&server) + .await?; + test.submit_turn(FIRST_PROMPT).await?; + let thread_ids = test.thread_manager.list_thread_ids().await; + assert_eq!(thread_ids.len(), 2); + let mut starts = Vec::new(); + for thread_id in thread_ids { + let thread = test.thread_manager.get_thread(thread_id).await?; + if thread_id != test.session_configured.thread_id { + wait_for_event(&thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; + } + thread.flush_rollout().await?; + let history = thread.load_history(/*include_archived*/ false).await?; + for item in history.items { + if let codex_history::RolloutItem::EventMsg(EventMsg::TurnStarted(event)) = item { + starts.push((thread_id, event)); + } + } + } + worker.single_request(); + assert_eq!(starts.len(), 2); + assert_ne!(starts[0].1.turn_id, starts[1].1.turn_id); + let root_turn_id = &starts + .iter() + .find(|(id, _)| *id == test.session_configured.thread_id) + .expect("root turn") + .1 + .turn_id; + assert!( + starts + .iter() + .all(|(_, event)| { event.root_turn_id.as_ref() == Some(root_turn_id) }) + ); + Ok(()) +} + #[derive(Clone, Copy, PartialEq, Eq)] enum ResidencyReload { Sender, diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index df73438792..3e4f6c199e 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -62,6 +62,7 @@ fn resume_history( history: Arc::new(vec![ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index 5ff41881cf..5afee5f17b 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -45,6 +45,7 @@ use core_test_support::submit_thread_settings; use core_test_support::test_codex::local; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; use test_case::test_case; @@ -123,20 +124,25 @@ async fn strict_tool_collisions_fail_the_turn_before_sampling( defer_loading: false, })] }; - let thread = test + let codex_core::NewThread { thread, .. } = test .thread_manager .start_thread(StartThreadOptions { dynamic_tools, ..StartThreadOptions::new(test.config.clone()) }) - .await? - .thread; + .await?; thread - .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { - text: "use the planning tool".to_string(), - text_elements: Vec::new(), - }])) + .start_or_steer_turn( + TurnInputRequest::user_input(vec![UserInput::Text { + text: "use the planning tool".to_string(), + text_elements: Vec::new(), + }]) + .on_start(codex_core::TurnStartOptions { + root_turn_id: Some("root-turn".into()), + ..Default::default() + }), + ) .await?; let EventMsg::Error(error) = @@ -157,6 +163,18 @@ async fn strict_tool_collisions_fail_the_turn_before_sampling( unreachable!("event predicate guarantees turn completion"); }; assert_eq!(completed.error, Some(error)); + thread.flush_rollout().await?; + let history = thread.load_history(/*include_archived*/ false).await?; + let attribution = history.items.iter().find_map(|item| match item { + codex_history::RolloutItem::EventMsg(EventMsg::TurnStarted(event)) + if event.turn_id == completed.turn_id => + { + event.root_turn_id.as_deref() + } + _ => None, + }); + assert_eq!(attribution, Some("root-turn")); + assert!( server .received_requests() diff --git a/codex-rs/external-agent-migration/src/sessions/export.rs b/codex-rs/external-agent-migration/src/sessions/export.rs index 2370bf1fc1..e3b360da3a 100644 --- a/codex-rs/external-agent-migration/src/sessions/export.rs +++ b/codex-rs/external-agent-migration/src/sessions/export.rs @@ -106,6 +106,7 @@ pub(super) fn rollout_items_from_messages(messages: Vec) -> items.push(RolloutItem::EventMsg(EventMsg::TurnStarted( TurnStartedEvent { turn_id: turn_id.clone(), + root_turn_id: None, trace_id: None, started_at, model_context_window: None, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e25cce88a0..0c79853a35 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2174,6 +2174,10 @@ pub struct TurnCompleteEvent { #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct TurnStartedEvent { pub turn_id: String, + /// ID of the originating turn in the root thread; equals `turn_id` for root turns. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub root_turn_id: Option, // Persist for rollout consumers that correlate turns with telemetry traces. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -4468,6 +4472,16 @@ mod tests { use tempfile::NamedTempFile; use tempfile::TempDir; + #[test] + fn old_turn_started_records_have_no_root_attribution() { + let event: TurnStartedEvent = serde_json::from_value(serde_json::json!({ + "turn_id": "old-turn", + "model_context_window": null + })) + .unwrap(); + assert_eq!(event.root_turn_id, None); + } + #[test] fn review_decision_denied_round_trip() -> Result<()> { let decision = ReviewDecision::Denied { diff --git a/codex-rs/rollout/src/persistence_metrics_tests.rs b/codex-rs/rollout/src/persistence_metrics_tests.rs index fed1b112e8..264ca889c6 100644 --- a/codex-rs/rollout/src/persistence_metrics_tests.rs +++ b/codex-rs/rollout/src/persistence_metrics_tests.rs @@ -42,6 +42,7 @@ fn retained_message(text: &str) -> RolloutItem { fn turn_started(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/thread-store/src/local/mod.rs b/codex-rs/thread-store/src/local/mod.rs index 37467db3e1..4e20031a50 100644 --- a/codex-rs/thread-store/src/local/mod.rs +++ b/codex-rs/thread-store/src/local/mod.rs @@ -1076,6 +1076,7 @@ mod tests { .append_items(&[RolloutItem::EventMsg(EventMsg::TurnStarted( TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/thread-store/src/local/model_context_tests.rs b/codex-rs/thread-store/src/local/model_context_tests.rs index 55cae4f6d2..0d7d90a892 100644 --- a/codex-rs/thread-store/src/local/model_context_tests.rs +++ b/codex-rs/thread-store/src/local/model_context_tests.rs @@ -776,6 +776,7 @@ fn append_items(path: &Path, items: impl IntoIterator) { fn turn_started(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: Some(128_000), diff --git a/codex-rs/thread-store/src/local/revert_thread_tests.rs b/codex-rs/thread-store/src/local/revert_thread_tests.rs index 137dbaeeee..0b27ac1b82 100644 --- a/codex-rs/thread-store/src/local/revert_thread_tests.rs +++ b/codex-rs/thread-store/src/local/revert_thread_tests.rs @@ -279,6 +279,7 @@ async fn turn_ids(store: &LocalThreadStore, thread_id: ThreadId) -> Vec fn turn_started(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: Some(10), model_context_window: None, diff --git a/codex-rs/thread-store/src/local/rollout_migration/canonicalizer.rs b/codex-rs/thread-store/src/local/rollout_migration/canonicalizer.rs index 13a80e37f8..8c446ccc5c 100644 --- a/codex-rs/thread-store/src/local/rollout_migration/canonicalizer.rs +++ b/codex-rs/thread-store/src/local/rollout_migration/canonicalizer.rs @@ -348,6 +348,7 @@ impl LegacyRolloutCanonicalizer { timestamp, RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id, + root_turn_id: None, trace_id: None, started_at: Some(started_at), model_context_window: None, diff --git a/codex-rs/thread-store/src/local/rollout_migration_tests.rs b/codex-rs/thread-store/src/local/rollout_migration_tests.rs index 473ead2502..8dfad90b9e 100644 --- a/codex-rs/thread-store/src/local/rollout_migration_tests.rs +++ b/codex-rs/thread-store/src/local/rollout_migration_tests.rs @@ -223,6 +223,7 @@ fn item_completed(turn_id: &str, item_id: &str) -> RolloutItem { fn started(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: Some(1_735_905_600), model_context_window: None, diff --git a/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs b/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs index 2ea0ac86ef..3f6fcbf615 100644 --- a/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs +++ b/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs @@ -2601,6 +2601,7 @@ async fn create_paginated_subagent_thread( fn turn_started(turn_id: &str) -> RolloutItem { RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: Some(10), model_context_window: None, diff --git a/codex-rs/thread-store/src/thread_metadata_sync.rs b/codex-rs/thread-store/src/thread_metadata_sync.rs index 791c0ff372..fb7d4d8c78 100644 --- a/codex-rs/thread-store/src/thread_metadata_sync.rs +++ b/codex-rs/thread-store/src/thread_metadata_sync.rs @@ -631,6 +631,7 @@ mod tests { .observe_appended_items(&[RolloutItem::EventMsg(EventMsg::TurnStarted( TurnStartedEvent { turn_id: "turn-1".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 1cb3cf299f..3397652530 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -7982,6 +7982,7 @@ async fn prompt_edit_forks_before_selected_prompt_and_preserves_source() -> Resu for item in [ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, diff --git a/codex-rs/tui/src/app/tests/pagination_completion_tests.rs b/codex-rs/tui/src/app/tests/pagination_completion_tests.rs index b0418b8324..54bd23ba1f 100644 --- a/codex-rs/tui/src/app/tests/pagination_completion_tests.rs +++ b/codex-rs/tui/src/app/tests/pagination_completion_tests.rs @@ -53,6 +53,7 @@ async fn older_pagination_completion_footers_follow_answers_without_overlap_dupl let finished = completed_at.timestamp() + index as i64 * 60; let mut events = vec![EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.clone(), + root_turn_id: None, trace_id: None, started_at: Some(finished - 125), model_context_window: None, diff --git a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs index f8d053236d..2b3fca5cb3 100644 --- a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs +++ b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs @@ -1824,6 +1824,7 @@ async fn older_pagination_reconciles_review_prompts_across_page_boundaries() -> ]); let events = std::iter::once(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "cross-page-review-turn".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -1986,6 +1987,7 @@ async fn transcript_home_loads_every_older_history_page() -> Result<()> { .collect::, _>>()?; let events = std::iter::once(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "multi-page-turn".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None, @@ -2394,6 +2396,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri .collect::, _>>()?; let events = std::iter::once(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "scrollback-pagination-turn".to_string(), + root_turn_id: None, trace_id: None, started_at: None, model_context_window: None,