@@ -57,7 +57,7 @@ function inferenceDone(
5757 } as unknown as ReactorInboundEvent ;
5858}
5959
60- function inferenceDoneText ( text : string ) : ReactorInboundEvent {
60+ function inferenceDoneText ( text : string , inputTokens = 0 ) : ReactorInboundEvent {
6161 return {
6262 type : "inference.done" ,
6363 turn : {
@@ -66,7 +66,7 @@ function inferenceDoneText(text: string): ReactorInboundEvent {
6666 timestamp : 0 ,
6767 content : [ { type : "text" , text } ] ,
6868 } ,
69- usage : { input : 0 , output : 1 , cacheRead : 0 , cacheWrite : 0 , thinking : 0 } ,
69+ usage : { input : inputTokens , output : 1 , cacheRead : 0 , cacheWrite : 0 , thinking : 0 } ,
7070 source : { model : "test-model" } ,
7171 } as unknown as ReactorInboundEvent ;
7272}
@@ -477,4 +477,78 @@ describe("SubAgentDirector post-complete terminalization (CL-7068)", () => {
477477 expect ( followup . some ( ( action ) => action . type === "infer" ) ) . toBe ( true ) ;
478478 expect ( followup . some ( ( action ) => action . type === "wait" ) ) . toBe ( false ) ;
479479 } ) ;
480+
481+ test ( "empty continuation after incomplete-report-stop salvage waits instead of re-inferring" , async ( ) => {
482+ const director = new SubAgentDirector ( "system" , [ ] , undefined , 1000 ) ;
483+ const caps = capabilities ( ) ;
484+
485+ await director . decide ( inferenceDone ( [ "read-1" ] ) , state , caps ) ;
486+ await director . decide ( toolDone ( "read-1" ) , state , caps ) ;
487+ await director . decide ( inferenceDoneText ( "Still looking at the files..." ) , state , caps ) ;
488+ const salvage = actions (
489+ await director . decide ( inferenceDoneText ( "Still narrating, no envelope." ) , state , caps ) ,
490+ ) ;
491+ expect ( salvage ) . toContainEqual ( { type : "checkpoint" , message : "subagent-incomplete-report" } ) ;
492+ expect ( salvage . some ( ( action ) => action . type === "reply" ) ) . toBe ( true ) ;
493+
494+ const afterEmpty = actions ( await director . decide ( messageReceived ( "" ) , state , caps ) ) ;
495+ expect ( afterEmpty . some ( ( action ) => action . type === "infer" ) ) . toBe ( false ) ;
496+ expect ( afterEmpty . some ( ( action ) => action . type === "reply" ) ) . toBe ( false ) ;
497+ expect ( afterEmpty ) . toContainEqual ( { type : "wait" } ) ;
498+ } ) ;
499+
500+ test ( "idle-compact meter path after a report reply waits instead of re-inferring" , async ( ) => {
501+ let continuations = 0 ;
502+ const director = new SubAgentDirector (
503+ "system" ,
504+ [ ] ,
505+ ( ) => {
506+ continuations ++ ;
507+ } ,
508+ 1000 ,
509+ ) ;
510+ const caps = capabilities ( ) ;
511+
512+ // Under-threshold tooling so tool.done does not compact before the report.
513+ await director . decide ( inferenceDone ( [ "read-1" ] ) , longState , caps ) ;
514+ await director . decide ( toolDone ( "read-1" ) , longState , caps ) ;
515+
516+ const complete = actions (
517+ await director . decide ( inferenceDoneText ( REPORT_ENVELOPE , 999_999 ) , longState , caps ) ,
518+ ) ;
519+ expect ( complete ) . toContainEqual ( { type : "checkpoint" , message : "subagent-complete" } ) ;
520+ expect ( complete . some ( ( action ) => action . type === "reply" ) ) . toBe ( true ) ;
521+ // noteIdleTurn arms a continuation so the idle-compact path can run.
522+ expect ( continuations ) . toBe ( 1 ) ;
523+
524+ const compact = actions ( await director . decide ( messageReceived ( "" ) , longState , caps ) ) ;
525+ expect ( compact ) . toEqual ( [
526+ { type : "compact" , compactor : "pruning-compactor" , reason : "context-threshold" } ,
527+ ] ) ;
528+ expect ( continuations ) . toBe ( 2 ) ;
529+
530+ // Post-compact empty re-entry is meter-only; reportReplied keeps it waiting.
531+ const afterMeter = actions ( await director . decide ( messageReceived ( "" ) , longState , caps ) ) ;
532+ expect ( afterMeter . some ( ( action ) => action . type === "infer" ) ) . toBe ( false ) ;
533+ expect ( afterMeter . some ( ( action ) => action . type === "reply" ) ) . toBe ( false ) ;
534+ expect ( afterMeter ) . toContainEqual ( { type : "wait" } ) ;
535+ } ) ;
536+
537+ test ( "repeated empty continuations after a report reply keep waiting" , async ( ) => {
538+ const director = new SubAgentDirector ( "system" , [ ] , undefined , 1000 ) ;
539+ const caps = capabilities ( ) ;
540+
541+ await director . decide ( inferenceDone ( [ "read-1" ] ) , state , caps ) ;
542+ await director . decide ( toolDone ( "read-1" ) , state , caps ) ;
543+ await director . decide ( inferenceDoneText ( REPORT_ENVELOPE ) , state , caps ) ;
544+
545+ const firstEmpty = actions ( await director . decide ( messageReceived ( "" ) , state , caps ) ) ;
546+ expect ( firstEmpty . some ( ( action ) => action . type === "infer" ) ) . toBe ( false ) ;
547+ expect ( firstEmpty ) . toContainEqual ( { type : "wait" } ) ;
548+
549+ const secondEmpty = actions ( await director . decide ( messageReceived ( "" ) , state , caps ) ) ;
550+ expect ( secondEmpty . some ( ( action ) => action . type === "infer" ) ) . toBe ( false ) ;
551+ expect ( secondEmpty . some ( ( action ) => action . type === "reply" ) ) . toBe ( false ) ;
552+ expect ( secondEmpty ) . toContainEqual ( { type : "wait" } ) ;
553+ } ) ;
480554} ) ;
0 commit comments