@@ -162,8 +162,9 @@ pub struct SessionActor {
162162 /// The live turn's epoch, as returned by `mark_turn_started` — the value
163163 /// the backend stamps onto this turn's events. Inbound stamped events
164164 /// whose stamp doesn't match are stragglers from a superseded/cancelled
165- /// turn and are dropped. `None` = no gating basis (no turn started yet,
166- /// or `mark_turn_started` failed) → stamped events fail open (applied).
165+ /// turn and are dropped. `None` = no live gating basis (no turn started,
166+ /// a normal turn finished, or `mark_turn_started` failed) → stamped events
167+ /// fail open (applied).
167168 current_epoch : Option < u64 > ,
168169 /// Sends that arrived while a turn was live. Supersede = cancel-then-send
169170 /// (the Zed pattern): the incoming Send cancels the running turn and
@@ -648,6 +649,12 @@ impl SessionActor {
648649 let ( status, delta, sweep_to) = match result {
649650 Ok ( stop_reason) => {
650651 let cancelled = stop_reason == "cancelled" ;
652+ // Normal completion may be followed by output from detached
653+ // work. With no live epoch, stamped session traffic fails open;
654+ // cancelled and failed turns retain their stale-event guard.
655+ if !cancelled {
656+ self . current_epoch = None ;
657+ }
651658 (
652659 SessionStatus :: Idle ,
653660 SessionDelta :: TurnFinished { stop_reason, turn_seq } ,
@@ -1063,9 +1070,10 @@ mod tests {
10631070 }
10641071
10651072 #[ tokio:: test]
1066- async fn late_event_from_old_turn_is_dropped ( ) {
1067- // An event stamped with a finished turn's epoch must not be applied
1068- // after that turn's terminal.
1073+ async fn late_assistant_event_after_normal_turn_is_applied ( ) {
1074+ // A normal turn may launch detached work that reports back after the
1075+ // prompt future resolves. Its assistant output still belongs in the
1076+ // session transcript even though the actor is already idle.
10691077 let ( handle, sink, mut gates, _conn) = setup_gated ( 1 ) ;
10701078 handle. control_tx . send ( Control :: Send ( "hi" . into ( ) ) ) . unwrap ( ) ;
10711079 settle ( ) . await ;
@@ -1077,17 +1085,19 @@ mod tests {
10771085 settle ( ) . await ;
10781086 gates. remove ( 0 ) . send ( "end_turn" . into ( ) ) . unwrap ( ) ;
10791087 settle ( ) . await ;
1080- let before = sink . 0 . lock ( ) . len ( ) ;
1081- // Straggler stamped with the dead turn's epoch: dropped entirely .
1088+ // A late assistant chunk stamped with the normally completed turn's
1089+ // epoch must still be applied .
10821090 handle
10831091 . stream_tx
1084- . send ( acp_stamped ( text_chunk_event ( "straggler " ) , 1 ) )
1092+ . send ( acp_stamped ( text_chunk_event ( "background result " ) , 1 ) )
10851093 . unwrap ( ) ;
10861094 settle ( ) . await ;
1087- assert_eq ! (
1088- sink. 0 . lock( ) . len( ) ,
1089- before,
1090- "an event stamped with a dead turn's epoch must produce no deltas"
1095+ assert ! (
1096+ sink. 0 . lock( ) . iter( ) . any( |e| matches!(
1097+ & e. delta,
1098+ SessionDelta :: TextChunk { delta, .. } if delta == "background result"
1099+ ) ) ,
1100+ "late assistant output after a normal finish must reach the transcript"
10911101 ) ;
10921102 }
10931103
0 commit comments