@@ -1179,6 +1179,54 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
11791179 expect ( firstAttempt . fastModeStartedAtMs ) . toBe ( secondAttempt . fastModeStartedAtMs ) ;
11801180 } ) ;
11811181
1182+ it ( "reuses durable user-turn proof across live model switch retries" , async ( ) => {
1183+ let fallbackInvocation = 0 ;
1184+ state . runWithModelFallbackMock . mockImplementation ( async ( params : FallbackRunnerParams ) => {
1185+ fallbackInvocation += 1 ;
1186+ const result = await params . run ( params . provider , params . model ) ;
1187+ if ( fallbackInvocation === 1 ) {
1188+ throw new LiveSessionModelSwitchError ( {
1189+ provider : "openai" ,
1190+ model : "gpt-5.4" ,
1191+ } ) ;
1192+ }
1193+ return {
1194+ result,
1195+ provider : params . provider ,
1196+ model : params . model ,
1197+ attempts : [ ] ,
1198+ } ;
1199+ } ) ;
1200+ state . runAgentAttemptMock . mockImplementation ( async ( attemptParams : unknown ) => {
1201+ const attempt = attemptParams as {
1202+ userTurnTranscriptRecorder ?: {
1203+ markRuntimePersisted : ( message : { role : "user" ; content : string } ) => void ;
1204+ } ;
1205+ } ;
1206+ if ( state . runAgentAttemptMock . mock . calls . length === 1 ) {
1207+ attempt . userTurnTranscriptRecorder ?. markRuntimePersisted ( {
1208+ role : "user" ,
1209+ content : "hello" ,
1210+ } ) ;
1211+ }
1212+ return makeSuccessResult ( "openai" , "gpt-5.4" ) ;
1213+ } ) ;
1214+
1215+ await runBasicAgentCommand ( ) ;
1216+
1217+ const firstAttempt = mockCallArg ( state . runAgentAttemptMock , 0 ) as {
1218+ suppressPromptPersistenceOnRetry ?: boolean ;
1219+ userTurnTranscriptRecorder ?: unknown ;
1220+ } ;
1221+ const secondAttempt = mockCallArg ( state . runAgentAttemptMock , 1 ) as {
1222+ suppressPromptPersistenceOnRetry ?: boolean ;
1223+ userTurnTranscriptRecorder ?: unknown ;
1224+ } ;
1225+ expect ( secondAttempt . userTurnTranscriptRecorder ) . toBe ( firstAttempt . userTurnTranscriptRecorder ) ;
1226+ expect ( firstAttempt . suppressPromptPersistenceOnRetry ) . toBe ( false ) ;
1227+ expect ( secondAttempt . suppressPromptPersistenceOnRetry ) . toBe ( true ) ;
1228+ } ) ;
1229+
11821230 it ( "uses an embedded queue rebound generation for terminal lifecycle and cleanup" , async ( ) => {
11831231 setupSingleAttemptFallback ( ) ;
11841232 state . runAgentAttemptMock . mockImplementation ( async ( attemptParams : unknown ) => {
@@ -2778,9 +2826,12 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
27782826 expect ( attemptCalls [ 1 ] ?. suppressPromptPersistenceOnRetry ) . toBe ( true ) ;
27792827 } ) ;
27802828
2781- it ( "suppresses prompt persistence for internal handoffs on every fallback attempt " , async ( ) => {
2829+ it ( "keeps a hook-blocked user turn suppressed across model fallback" , async ( ) => {
27822830 type AttemptCall = {
27832831 suppressPromptPersistenceOnRetry ?: boolean ;
2832+ userTurnTranscriptRecorder ?: {
2833+ markBlocked : ( ) => void ;
2834+ } ;
27842835 } ;
27852836 const attemptCalls : AttemptCall [ ] = [ ] ;
27862837 state . runWithModelFallbackMock . mockImplementation ( async ( params : FallbackRunnerParams ) => {
@@ -2795,9 +2846,53 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
27952846 } ) ;
27962847 state . runAgentAttemptMock . mockImplementation ( async ( attemptParams : AttemptCall ) => {
27972848 attemptCalls . push ( attemptParams ) ;
2849+ if ( attemptCalls . length === 1 ) {
2850+ attemptParams . userTurnTranscriptRecorder ?. markBlocked ( ) ;
2851+ }
27982852 return makeSuccessResult ( "openai" , "gpt-5.4" ) ;
27992853 } ) ;
28002854
2855+ await runBasicAgentCommand ( ) ;
2856+
2857+ expect ( attemptCalls ) . toHaveLength ( 2 ) ;
2858+ expect ( attemptCalls [ 1 ] ?. userTurnTranscriptRecorder ) . toBe (
2859+ attemptCalls [ 0 ] ?. userTurnTranscriptRecorder ,
2860+ ) ;
2861+ expect ( attemptCalls [ 0 ] ?. suppressPromptPersistenceOnRetry ) . toBe ( false ) ;
2862+ expect ( attemptCalls [ 1 ] ?. suppressPromptPersistenceOnRetry ) . toBe ( true ) ;
2863+ } ) ;
2864+
2865+ it ( "suppresses prompt persistence for internal handoffs on every fallback attempt" , async ( ) => {
2866+ type AttemptCall = {
2867+ suppressPromptPersistenceOnRetry ?: boolean ;
2868+ } ;
2869+ const attemptCalls : AttemptCall [ ] = [ ] ;
2870+ state . runWithModelFallbackMock . mockImplementation ( async ( params : FallbackRunnerParams ) => {
2871+ const first = await params . run ( params . provider , params . model ) ;
2872+ const result = await params . run ( params . provider , params . model ) ;
2873+ return {
2874+ result,
2875+ provider : params . provider ,
2876+ model : params . model ,
2877+ attempts : [ first ] ,
2878+ } ;
2879+ } ) ;
2880+ state . runAgentAttemptMock . mockImplementation ( async ( attemptParams : AttemptCall ) => {
2881+ attemptCalls . push ( attemptParams ) ;
2882+ const result = makeSuccessResult ( "openai" , "gpt-5.4" ) as ReturnType <
2883+ typeof makeSuccessResult
2884+ > & {
2885+ meta : Record < string , unknown > & { executionTrace ?: Record < string , unknown > } ;
2886+ } ;
2887+ result . meta . executionTrace = {
2888+ runner : "cli" ,
2889+ fallbackUsed : false ,
2890+ winnerProvider : "openai" ,
2891+ winnerModel : "gpt-5.4" ,
2892+ } ;
2893+ return result ;
2894+ } ) ;
2895+
28012896 await agentCommand ( {
28022897 message : "internal handoff" ,
28032898 to : "+1234567890" ,
@@ -2807,6 +2902,51 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
28072902 expect ( attemptCalls ) . toHaveLength ( 2 ) ;
28082903 expect ( attemptCalls [ 0 ] ?. suppressPromptPersistenceOnRetry ) . toBe ( true ) ;
28092904 expect ( attemptCalls [ 1 ] ?. suppressPromptPersistenceOnRetry ) . toBe ( true ) ;
2905+ expectRecordFields ( mockCallArg ( state . persistCliTurnTranscriptMock ) , {
2906+ skipUserTurn : true ,
2907+ } ) ;
2908+ } ) ;
2909+
2910+ it ( "preserves an explicit empty transcript message as user-turn omission" , async ( ) => {
2911+ setupSingleAttemptFallback ( ) ;
2912+ state . runAgentAttemptMock . mockResolvedValue ( makeSuccessResult ( "openai" , "gpt-5.4" ) ) ;
2913+
2914+ await agentCommand ( {
2915+ message : "synthetic announce prompt" ,
2916+ transcriptMessage : "" ,
2917+ to : "+1234567890" ,
2918+ } ) ;
2919+
2920+ const attempt = mockCallArg ( state . runAgentAttemptMock ) as {
2921+ suppressPromptPersistenceOnRetry ?: boolean ;
2922+ userTurnTranscriptRecorder ?: { message ?: unknown } ;
2923+ } ;
2924+ expect ( attempt . suppressPromptPersistenceOnRetry ) . toBe ( true ) ;
2925+ expect ( attempt . userTurnTranscriptRecorder ?. message ) . toBeUndefined ( ) ;
2926+ } ) ;
2927+
2928+ it ( "uses a tracker-only recorder for text plus image turns" , async ( ) => {
2929+ setupSingleAttemptFallback ( ) ;
2930+ state . runAgentAttemptMock . mockResolvedValue ( makeSuccessResult ( "openai" , "gpt-5.4" ) ) ;
2931+
2932+ await agentCommand ( {
2933+ message : "inspect this image" ,
2934+ transcriptMessage : "canonical image caption" ,
2935+ images : [ { type : "image" , data : "aGVsbG8=" , mimeType : "image/png" } ] ,
2936+ to : "+1234567890" ,
2937+ } ) ;
2938+
2939+ const attempt = mockCallArg ( state . runAgentAttemptMock ) as {
2940+ transcriptBody ?: string ;
2941+ suppressPromptPersistenceOnRetry ?: boolean ;
2942+ userTurnTranscriptRecorder ?: { message ?: unknown } ;
2943+ } ;
2944+ expect ( attempt . transcriptBody ) . toBe ( "canonical image caption" ) ;
2945+ expect ( attempt . suppressPromptPersistenceOnRetry ) . toBe ( false ) ;
2946+ expect ( attempt . userTurnTranscriptRecorder ?. message ) . toMatchObject ( {
2947+ role : "user" ,
2948+ content : "canonical image caption" ,
2949+ } ) ;
28102950 } ) ;
28112951
28122952 it ( "propagates non-switch errors without retrying and emits lifecycle error" , async ( ) => {
0 commit comments