@@ -311,9 +311,10 @@ export interface SessionBridge {
311311 * Stall bound for a silent ask-wake primary turn (CL-8016). If a wake was
312312 * actually sent (`askWakeTurnArmed`) and `shouldAbortForStall` says the
313313 * turn is silent past the bound (including awaiting-first-token, per
314- * #1095), un-dedupe the delivered wakes and interrupt so the questions
315- * re-surface escalated. Returns true when it aborted. Driven by the fleet
316- * stall poll, which settles deadline-past asks first.
314+ * #1095), interrupt the hung inference, un-dedupe the delivered wakes,
315+ * and hand the next turn to occupancy (mailbox mail) before any re-surface.
316+ * Returns true when it aborted. Driven by the fleet stall poll, which
317+ * settles deadline-past asks first, and by the #1095 monitor tick.
317318 */
318319 abortStalledWakeTurn : ( ) => boolean ;
319320 /**
@@ -1383,9 +1384,9 @@ function applyInbound(
13831384 bag . askWakeResurface . delete ( sessionId ) ;
13841385 }
13851386 }
1386- // A fresh snapshot with nothing pending means every parked question
1387- // settled or expired: no wake turn is owed, so disarm the bound.
1388- if ( bag . pendingAskWake . size === 0 ) bag . askWakeTurnArmed = false ;
1387+ // A fresh snapshot with nothing pending means no wake is owed, but a
1388+ // still-armed silent turn must stay armed so the stall abort can interrupt
1389+ // it. Disarming here (expire / send_input) would leave isProcessing hung.
13891390 bag . flushPendingAskWake ?.( ) ;
13901391 return ;
13911392 }
@@ -1900,30 +1901,11 @@ export function attachSessionBridge(
19001901 } ;
19011902
19021903 /**
1903- * Stall bound for a silent ask-wake primary turn (CL-8016). Only an armed
1904- * (wake-sent, never settled) turn can match. Silence uses `shouldAbortForStall`
1905- * — the same #1095 bound that already covers awaiting-first-token — then
1906- * un-dedupes and interrupts so parked questions re-surface. Mail first so
1907- * occupancy can take the next turn; the trailing wake flush restates if
1908- * mail did not start one.
1904+ * Shared abort used by Ctrl+C, the #1095 monitor tick, and the fleet-poll
1905+ * stall bound. Interrupt the hung inference before any new deliver, then
1906+ * occupancy/mailbox first so a re-surface wake cannot steal that turn.
19091907 */
1910- const abortStalledWakeTurn = ( ) : boolean => {
1911- if ( bag . disposed || ! bag . askWakeTurnArmed ) return false ;
1912- if ( ! shouldAbortForStall ( stallArgsFor ( now ( ) ) ) ) return false ;
1913- const layer = turnStallLayer ( bag . turn ) ?? "mid-stream" ;
1914- unDedupeArmedAskWakes ( ) ;
1915- recordTurnMarker ( bag , `stall-abort:${ layer } ` ) ;
1916- bag . askWakeTurnArmed = false ;
1917- bag . turn = turnStateOnInterrupt ( bag . turn , now ( ) ) ;
1918- paintPhase ( ) ;
1919- drainAtBoundary ( shell , bag ) ;
1920- flushMailboxMail ( ) ;
1921- flushPendingAskWake ( ) ;
1922- return true ;
1923- } ;
1924-
1925- const doInterrupt = ( ) : void => {
1926- if ( bag . disposed ) return ;
1908+ const abortInFlightAndHandoff = ( ) : void => {
19271909 closeOpenRow ( shell , bag ) ;
19281910 bag . pendingEchoes . length = 0 ;
19291911 // The stopped attempt is no longer in flight. Expire the error-recovery
@@ -1942,14 +1924,37 @@ export function attachSessionBridge(
19421924 // turn the operator (or the watchdog) deliberately stopped.
19431925 recordLastSent ( null ) ;
19441926 bag . awaitingContinuationInference = false ;
1945- // Armed wake that dies here (operator stop or #1095 stall abort) never
1946- // landed: un-dedupe so flushPendingAskWake restates the questions.
1927+ // Armed wake that dies here (operator stop or stall abort) never
1928+ // landed: un-dedupe so flushPendingAskWake can restate the questions
1929+ // if occupancy does not take the next turn.
19471930 unDedupeArmedAskWakes ( ) ;
19481931 bag . askWakeTurnArmed = false ;
19491932 bag . turn = turnStateOnInterrupt ( bag . turn , now ( ) ) ;
19501933 paintPhase ( ) ;
1951- flushPendingAskWake ( ) ;
19521934 flushMailboxMail ( ) ;
1935+ if ( ! bag . turn . isProcessing ) flushPendingAskWake ( ) ;
1936+ } ;
1937+
1938+ /**
1939+ * Stall bound for a silent ask-wake primary turn (CL-8016). Only an armed
1940+ * (wake-sent, never settled) turn can match. Silence uses `shouldAbortForStall`
1941+ * — the same #1095 bound that already covers awaiting-first-token — then
1942+ * interrupts the hung inference before any new deliver. Mail first so
1943+ * occupancy can take the next turn; the trailing wake flush restates only
1944+ * if mail did not start one.
1945+ */
1946+ const abortStalledWakeTurn = ( ) : boolean => {
1947+ if ( bag . disposed || ! bag . askWakeTurnArmed ) return false ;
1948+ if ( ! shouldAbortForStall ( stallArgsFor ( now ( ) ) ) ) return false ;
1949+ const layer = turnStallLayer ( bag . turn ) ?? "mid-stream" ;
1950+ recordTurnMarker ( bag , `stall-abort:${ layer } ` ) ;
1951+ abortInFlightAndHandoff ( ) ;
1952+ return true ;
1953+ } ;
1954+
1955+ const doInterrupt = ( ) : void => {
1956+ if ( bag . disposed ) return ;
1957+ abortInFlightAndHandoff ( ) ;
19531958 } ;
19541959 const clearQueuedDelivery = ( ) : void => {
19551960 if ( bag . disposed ) return ;
@@ -2052,7 +2057,10 @@ export function attachSessionBridge(
20522057 if ( shouldAbortForStall ( stallArgs ) ) {
20532058 applyStallRecovery (
20542059 {
2055- abort : doInterrupt ,
2060+ abort : ( ) => {
2061+ if ( abortStalledWakeTurn ( ) ) return ;
2062+ doInterrupt ( ) ;
2063+ } ,
20562064 notify : ( message ) =>
20572065 setStatusFlash ( shell , message , { ttlMs : RUNTIME_FLASH_MS } ) ,
20582066 } ,
0 commit comments