@@ -176,7 +176,11 @@ export class SubAgentDirector extends DefaultDirector {
176176 private readonly stallTimeoutMs : number | undefined ;
177177 private readonly now : ( ) => number ;
178178 private lastActivityAt : number ;
179- private consecutiveStalls = 0 ;
179+ // Wall clock when the first stall nudge was issued. Later empty pings inside
180+ // stallTimeoutMs of this instant wait without stopping or restarting grace;
181+ // stop only after the grace elapses with no activity. Cleared on real
182+ // tool.done / turn-boundary activity.
183+ private stallNudgeAt : number | undefined ;
180184 private lastAssistantText = "" ;
181185 // Every stop and nudge is recorded with its measured value beside its
182186 // threshold, so a later threshold change can cite data instead of judgment
@@ -303,7 +307,7 @@ export class SubAgentDirector extends DefaultDirector {
303307 if ( onTurnBoundary ( event ) ) {
304308 this . lastConsumedNudgeText = null ;
305309 this . lastActivityAt = this . now ( ) ;
306- this . consecutiveStalls = 0 ;
310+ this . stallNudgeAt = undefined ;
307311 this . compaction . noteInferenceDone ( event , state . turns ) ;
308312 this . turnsCompleted ++ ;
309313 const content = event . turn . content as readonly {
@@ -410,7 +414,7 @@ export class SubAgentDirector extends DefaultDirector {
410414 }
411415 if ( event . type === "tool.done" ) {
412416 this . lastActivityAt = this . now ( ) ;
413- this . consecutiveStalls = 0 ;
417+ this . stallNudgeAt = undefined ;
414418 if ( event . result . isError === true ) {
415419 // Failed-tool recovery guidance. Arm once; coalesce consecutive failure
416420 // audits until applyPendingNudge flushes a single counted record.
@@ -438,12 +442,12 @@ export class SubAgentDirector extends DefaultDirector {
438442 * "no pending harness-tracked work" falls out of when this method can run
439443 * at all rather than needing separate bookkeeping.
440444 *
441- * First stall past the timeout: one continuation nudge, asking the leaf to
442- * report status or keep going. A second consecutive stall (no activity
443- * since the nudge) escalates to the existing salvage path, same shape as
444- * the turn-boundary checks above. Returns null when this event is not
445- * a stall check the director should act on (let it fall through as an
446- * ordinary continuation).
445+ * First silence past the timeout: one continuation nudge, and record
446+ * stallNudgeAt. Queued pings that arrive inside the stallTimeoutMs grace
447+ * after that nudge neither stop nor restart the grace (and do not count as
448+ * activity). Stop only when a ping arrives after the grace with still no
449+ * activity. Returns null when this event is not a stall check the director
450+ * should act on (let it fall through as an ordinary continuation).
447451 */
448452 private checkStallPing (
449453 event : ReactorInboundEvent ,
@@ -456,8 +460,8 @@ export class SubAgentDirector extends DefaultDirector {
456460 const elapsed = this . now ( ) - this . lastActivityAt ;
457461 if ( elapsed < this . stallTimeoutMs ) return null ;
458462
459- this . consecutiveStalls ++ ;
460- if ( this . consecutiveStalls === 1 ) {
463+ if ( this . stallNudgeAt === undefined ) {
464+ this . stallNudgeAt = this . now ( ) ;
461465 this . interventions ( {
462466 id : "stall-nudge" ,
463467 class : "nudge" ,
@@ -473,6 +477,14 @@ export class SubAgentDirector extends DefaultDirector {
473477 inferWithSubAgentNudge ( capabilities , SUBAGENT_STALL_NUDGE ) ,
474478 ] ;
475479 }
480+
481+ const sinceNudge = this . now ( ) - this . stallNudgeAt ;
482+ if ( sinceNudge < this . stallTimeoutMs ) {
483+ // Still inside the post-nudge grace. Wait without faking activity or
484+ // restarting the grace clock.
485+ return [ capabilities . wait ( ) ] ;
486+ }
487+
476488 this . interventions ( {
477489 id : "stalled" ,
478490 class : "stop" ,
0 commit comments