@@ -172,7 +172,11 @@ export class SubAgentDirector extends DefaultDirector {
172172 private readonly stallTimeoutMs : number | undefined ;
173173 private readonly now : ( ) => number ;
174174 private lastActivityAt : number ;
175- private consecutiveStalls = 0 ;
175+ // Wall clock when the first stall nudge was issued. Later empty pings inside
176+ // stallTimeoutMs of this instant wait without stopping or restarting grace;
177+ // stop only after the grace elapses with no activity. Cleared on real
178+ // tool.done / turn-boundary activity.
179+ private stallNudgeAt : number | undefined ;
176180 private lastAssistantText = "" ;
177181 // Every stop and nudge is recorded with its measured value beside its
178182 // threshold, so a later threshold change can cite data instead of judgment
@@ -299,7 +303,7 @@ export class SubAgentDirector extends DefaultDirector {
299303 if ( onTurnBoundary ( event ) ) {
300304 this . lastConsumedNudgeText = null ;
301305 this . lastActivityAt = this . now ( ) ;
302- this . consecutiveStalls = 0 ;
306+ this . stallNudgeAt = undefined ;
303307 this . compaction . noteInferenceDone ( event , state . turns ) ;
304308 this . turnsCompleted ++ ;
305309 const content = event . turn . content as readonly {
@@ -406,7 +410,7 @@ export class SubAgentDirector extends DefaultDirector {
406410 }
407411 if ( event . type === "tool.done" ) {
408412 this . lastActivityAt = this . now ( ) ;
409- this . consecutiveStalls = 0 ;
413+ this . stallNudgeAt = undefined ;
410414 if ( event . result . isError === true ) {
411415 // Failed-tool recovery guidance. Arm once; coalesce consecutive failure
412416 // audits until applyPendingNudge flushes a single counted record.
@@ -434,12 +438,12 @@ export class SubAgentDirector extends DefaultDirector {
434438 * "no pending harness-tracked work" falls out of when this method can run
435439 * at all rather than needing separate bookkeeping.
436440 *
437- * First stall past the timeout: one continuation nudge, asking the leaf to
438- * report status or keep going. A second consecutive stall (no activity
439- * since the nudge) escalates to the existing salvage path, same shape as
440- * the turn-boundary checks above. Returns null when this event is not
441- * a stall check the director should act on (let it fall through as an
442- * ordinary continuation).
441+ * First silence past the timeout: one continuation nudge, and record
442+ * stallNudgeAt. Queued pings that arrive inside the stallTimeoutMs grace
443+ * after that nudge neither stop nor restart the grace (and do not count as
444+ * activity). Stop only when a ping arrives after the grace with still no
445+ * activity. Returns null when this event is not a stall check the director
446+ * should act on (let it fall through as an ordinary continuation).
443447 */
444448 private checkStallPing (
445449 event : ReactorInboundEvent ,
@@ -452,8 +456,8 @@ export class SubAgentDirector extends DefaultDirector {
452456 const elapsed = this . now ( ) - this . lastActivityAt ;
453457 if ( elapsed < this . stallTimeoutMs ) return null ;
454458
455- this . consecutiveStalls ++ ;
456- if ( this . consecutiveStalls === 1 ) {
459+ if ( this . stallNudgeAt === undefined ) {
460+ this . stallNudgeAt = this . now ( ) ;
457461 this . interventions ( {
458462 id : "stall-nudge" ,
459463 class : "nudge" ,
@@ -469,6 +473,14 @@ export class SubAgentDirector extends DefaultDirector {
469473 inferWithSubAgentNudge ( capabilities , SUBAGENT_STALL_NUDGE ) ,
470474 ] ;
471475 }
476+
477+ const sinceNudge = this . now ( ) - this . stallNudgeAt ;
478+ if ( sinceNudge < this . stallTimeoutMs ) {
479+ // Still inside the post-nudge grace. Wait without faking activity or
480+ // restarting the grace clock.
481+ return [ capabilities . wait ( ) ] ;
482+ }
483+
472484 this . interventions ( {
473485 id : "stalled" ,
474486 class : "stop" ,
0 commit comments