@@ -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 {
@@ -401,7 +405,7 @@ export class SubAgentDirector extends DefaultDirector {
401405 }
402406 if ( event . type === "tool.done" ) {
403407 this . lastActivityAt = this . now ( ) ;
404- this . consecutiveStalls = 0 ;
408+ this . stallNudgeAt = undefined ;
405409 if ( event . result . isError === true ) {
406410 // Failed-tool recovery guidance. Arm once; coalesce consecutive failure
407411 // audits until applyPendingNudge flushes a single counted record.
@@ -429,12 +433,12 @@ export class SubAgentDirector extends DefaultDirector {
429433 * "no pending harness-tracked work" falls out of when this method can run
430434 * at all rather than needing separate bookkeeping.
431435 *
432- * First stall past the timeout: one continuation nudge, asking the leaf to
433- * report status or keep going. A second consecutive stall (no activity
434- * since the nudge) escalates to the existing salvage path, same shape as
435- * the turn-boundary checks above. Returns null when this event is not
436- * a stall check the director should act on (let it fall through as an
437- * ordinary continuation).
436+ * First silence past the timeout: one continuation nudge, and record
437+ * stallNudgeAt. Queued pings that arrive inside the stallTimeoutMs grace
438+ * after that nudge neither stop nor restart the grace (and do not count as
439+ * activity). Stop only when a ping arrives after the grace with still no
440+ * activity. Returns null when this event is not a stall check the director
441+ * should act on (let it fall through as an ordinary continuation).
438442 */
439443 private checkStallPing (
440444 event : ReactorInboundEvent ,
@@ -447,8 +451,8 @@ export class SubAgentDirector extends DefaultDirector {
447451 const elapsed = this . now ( ) - this . lastActivityAt ;
448452 if ( elapsed < this . stallTimeoutMs ) return null ;
449453
450- this . consecutiveStalls ++ ;
451- if ( this . consecutiveStalls === 1 ) {
454+ if ( this . stallNudgeAt === undefined ) {
455+ this . stallNudgeAt = this . now ( ) ;
452456 this . interventions ( {
453457 id : "stall-nudge" ,
454458 class : "nudge" ,
@@ -464,6 +468,14 @@ export class SubAgentDirector extends DefaultDirector {
464468 inferWithSubAgentNudge ( capabilities , SUBAGENT_STALL_NUDGE ) ,
465469 ] ;
466470 }
471+
472+ const sinceNudge = this . now ( ) - this . stallNudgeAt ;
473+ if ( sinceNudge < this . stallTimeoutMs ) {
474+ // Still inside the post-nudge grace. Wait without faking activity or
475+ // restarting the grace clock.
476+ return [ capabilities . wait ( ) ] ;
477+ }
478+
467479 this . interventions ( {
468480 id : "stalled" ,
469481 class : "stop" ,
0 commit comments