@@ -70,7 +70,15 @@ function isStoredTransportSession(record: Pick<SessionRecord, 'runtimeType' | 'a
7070function shouldAutoRelaunchTransportRuntimeAfterError (
7171 providerError : TransportSessionRuntime [ 'lastProviderError' ] ,
7272) : boolean {
73- return providerError ?. code === PROVIDER_ERROR_CODES . CONNECTION_LOST ;
73+ if ( ! providerError ) return false ;
74+ if ( providerError . code === PROVIDER_ERROR_CODES . CONNECTION_LOST ) return true ;
75+ // Codex SDK can occasionally keep an internal "running turn" marker even
76+ // though the daemon has no active work left. Manual daemon restart clears the
77+ // stale provider state; treat repeated recoverable "already busy" failures as
78+ // the same relaunchable provider-wedged condition instead of leaving the UI in
79+ // a bare error state forever.
80+ return providerError . code === PROVIDER_ERROR_CODES . PROVIDER_ERROR
81+ && / a l r e a d y b u s y | s e s s i o n i s b u s y | p r o v i d e r i s b u s y / i. test ( providerError . message ) ;
7482}
7583
7684function sanitizeCodexSdkStartupModel ( value : string | null | undefined ) : string | undefined {
@@ -1156,7 +1164,7 @@ async function recoverTransportRuntimeAfterError(
11561164 pendingCount : runtime . pendingCount ,
11571165 activeDispatchCount : runtime . activeDispatchEntries . length ,
11581166 } ,
1159- 'Transport runtime error did not indicate provider connection loss ; skipping provider relaunch' ,
1167+ 'Transport runtime error did not indicate a relaunchable provider failure ; skipping provider relaunch' ,
11601168 ) ;
11611169 return false ;
11621170 }
@@ -1169,7 +1177,7 @@ async function recoverTransportRuntimeAfterError(
11691177 providerError,
11701178 ...preservation ,
11711179 } ,
1172- 'Transport provider connection lost — preserving queues and relaunching provider runtime' ,
1180+ 'Transport provider failure — preserving queues and relaunching provider runtime' ,
11731181 ) ;
11741182
11751183 const now = Date . now ( ) ;
@@ -1198,8 +1206,11 @@ async function recoverTransportRuntimeAfterError(
11981206
11991207 if ( pendingCount > 0 ) {
12001208 const queued = getResendEntries ( sessionName ) ;
1209+ const recoveryReason = providerError ?. code === PROVIDER_ERROR_CODES . CONNECTION_LOST
1210+ ? 'Provider connection lost'
1211+ : 'Provider became stuck busy' ;
12011212 timelineEmitter . emit ( sessionName , 'assistant.text' , {
1202- text : `⏳ Provider connection lost — auto-resending ${ pendingCount } queued message${ pendingCount === 1 ? '' : 's' } after recovery.` ,
1213+ text : `⏳ ${ recoveryReason } — auto-resending ${ pendingCount } queued message${ pendingCount === 1 ? '' : 's' } after recovery.` ,
12031214 streaming : false ,
12041215 memoryExcluded : true ,
12051216 } , { source : 'daemon' , confidence : 'high' } ) ;
@@ -1304,22 +1315,44 @@ async function drainTransportResendQueueIntoRuntime(
13041315 entry . commandId ,
13051316 attachments . length > 0 ? attachments : undefined ,
13061317 entry . messagePreamble ,
1307- sharedMetadata ,
1318+ {
1319+ ...sharedMetadata ,
1320+ ...( entry . timelineCommitted ? { timelineCommitted : true } : { } ) ,
1321+ ...( entry . historyCommitted ? { historyCommitted : true } : { } ) ,
1322+ } ,
13081323 )
13091324 : runtime . send (
13101325 entry . text ,
13111326 entry . commandId ,
13121327 attachments . length > 0 ? attachments : undefined ,
13131328 entry . messagePreamble ,
1329+ {
1330+ ...( entry . timelineCommitted ? { timelineCommitted : true } : { } ) ,
1331+ ...( entry . historyCommitted ? { historyCommitted : true } : { } ) ,
1332+ } ,
13141333 ) )
13151334 : ( attachments . length > 0
13161335 ? ( sharedMetadata
1317- ? runtime . send ( entry . text , entry . commandId , attachments , undefined , sharedMetadata )
1318- : runtime . send ( entry . text , entry . commandId , attachments ) )
1336+ ? runtime . send ( entry . text , entry . commandId , attachments , undefined , {
1337+ ...sharedMetadata ,
1338+ ...( entry . timelineCommitted ? { timelineCommitted : true } : { } ) ,
1339+ ...( entry . historyCommitted ? { historyCommitted : true } : { } ) ,
1340+ } )
1341+ : runtime . send ( entry . text , entry . commandId , attachments , undefined , {
1342+ ...( entry . timelineCommitted ? { timelineCommitted : true } : { } ) ,
1343+ ...( entry . historyCommitted ? { historyCommitted : true } : { } ) ,
1344+ } ) )
13191345 : ( sharedMetadata
1320- ? runtime . send ( entry . text , entry . commandId , undefined , undefined , sharedMetadata )
1321- : runtime . send ( entry . text , entry . commandId ) ) ) ;
1322- if ( result === 'sent' ) {
1346+ ? runtime . send ( entry . text , entry . commandId , undefined , undefined , {
1347+ ...sharedMetadata ,
1348+ ...( entry . timelineCommitted ? { timelineCommitted : true } : { } ) ,
1349+ ...( entry . historyCommitted ? { historyCommitted : true } : { } ) ,
1350+ } )
1351+ : runtime . send ( entry . text , entry . commandId , undefined , undefined , {
1352+ ...( entry . timelineCommitted ? { timelineCommitted : true } : { } ) ,
1353+ ...( entry . historyCommitted ? { historyCommitted : true } : { } ) ,
1354+ } ) ) ) ;
1355+ if ( result === 'sent' && ! entry . timelineCommitted ) {
13231356 timelineEmitter . emit (
13241357 sessionName ,
13251358 'user.message' ,
@@ -1341,6 +1374,14 @@ async function drainTransportResendQueueIntoRuntime(
13411374 pendingMessageEntries : runtime . pendingEntries ,
13421375 pendingMessageVersion : observeTransportQueueRevision ( sessionName , runtime . pendingVersion ) ,
13431376 } , { source : 'daemon' , confidence : 'high' } ) ;
1377+ } else if ( result === 'sent' ) {
1378+ timelineEmitter . emit ( sessionName , 'session.state' , {
1379+ state : 'running' ,
1380+ pendingCount : runtime . pendingCount ,
1381+ pendingMessages : runtime . pendingMessages ,
1382+ pendingMessageEntries : runtime . pendingEntries ,
1383+ pendingMessageVersion : observeTransportQueueRevision ( sessionName , runtime . pendingVersion ) ,
1384+ } , { source : 'daemon' , confidence : 'high' } ) ;
13441385 } else if ( result === 'queued' ) {
13451386 timelineEmitter . emit ( sessionName , 'session.state' , {
13461387 state : 'queued' ,
@@ -1422,6 +1463,8 @@ function wireTransportCallbacks(runtime: TransportSessionRuntime, sessionName: s
14221463 payload . pendingMessages = runtime . pendingMessages ;
14231464 payload . pendingMessageEntries = runtime . pendingEntries ;
14241465 payload . pendingMessageVersion = observeTransportQueueRevision ( sessionName , runtime . pendingVersion ) ;
1466+ } else if ( mapped === 'error' && runtime . lastProviderError ?. message ) {
1467+ payload . error = runtime . lastProviderError . message ;
14251468 }
14261469 timelineEmitter . emit ( sessionName , 'session.state' , payload , { source : 'daemon' , confidence : 'high' } ) ;
14271470 if ( status === 'error' ) {
@@ -1448,7 +1491,7 @@ function wireTransportCallbacks(runtime: TransportSessionRuntime, sessionName: s
14481491 { source : 'daemon' , confidence : 'high' , eventId : transportUserEventId ( entry . clientMessageId ) } ,
14491492 ) ;
14501493 }
1451- if ( messages . length === 0 ) {
1494+ if ( messages . length === 0 && count === 0 ) {
14521495 timelineEmitter . emit ( sessionName , 'user.message' , { text : merged , batchedCount : count , allowDuplicate : true , pendingMessageVersion : drainedVersion } ) ;
14531496 }
14541497 // Include authoritative pending state after drain. The drained messages have
0 commit comments